mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python
@ 2026-10-18 11:06 Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 1/7] scripts: convert checkdeclares.pl " Breno Rodrigues Alves
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

This series converts seven more Perl utilities in scripts/ to
Python 3, following the checkpatch.py + bootgraph.py +
checkincludes.py + markup_oops.py + profile2linkerlist.py
series already submitted.

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:
  - checkdeclares.pl          -> checkdeclares.py
  - checkversion.pl           -> checkversion.py
  - extract_xc3028.pl         -> extract_xc3028.py
  - generate_initcall_order.pl -> generate_initcall_order.py
  - get_maintainer.pl         -> get_maintainer.py
  - leaking_addresses.pl      -> leaking_addresses.py
  - recordmcount.pl           -> recordmcount.py

Verified byte-for-byte identical to the Perl versions for the
scripts that have deterministic output. recordmcount.py tested
for x86_64 and arm64.

Not implemented in get_maintainer.py: --interactive, --self-test,
--git-blame. These can be added in follow-up patches.

Breno Rodrigues Alves (7):
  scripts: convert checkdeclares.pl to Python
  scripts: convert checkversion.pl to Python
  scripts: convert extract_xc3028.pl to Python
  scripts: convert generate_initcall_order.pl to Python
  scripts: convert get_maintainer.pl to Python
  scripts: convert leaking_addresses.pl to Python
  scripts: convert recordmcount.pl to Python

-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [RFC PATCH 1/7] scripts: convert checkdeclares.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 2/7] scripts: convert checkversion.pl " Breno Rodrigues Alves
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of checkdeclares.pl in Python 3
(stdlib only). Same output, same detection logic.

Tested byte-for-byte against the Perl version.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/checkdeclares.py | 64 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 64 insertions(+)
 create mode 100755 scripts/checkdeclares.py

diff --git a/scripts/checkdeclares.py b/scripts/checkdeclares.py
new file mode 100755
index 000000000..32c826ac8
--- /dev/null
+++ b/scripts/checkdeclares.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# checkdeclares: find struct declared more than once
+#
+# Copyright 2021 Wan Jiabing <wanjiabing@vivo.com>
+# Inspired by checkincludes.pl
+#
+# This script checks for duplicate struct declares.
+# Note that this will not take into consideration macros so
+# you should run this only if you know you do have real dups
+# and do not have them under #ifdef's.
+
+import re
+import sys
+
+RE_STRUCT_DECL = re.compile(r'^\s*struct\s*(\w*);$')
+
+
+def usage():
+    print('Usage: checkdeclares.py file1.h ...', file=sys.stderr)
+    print('Warns of struct declaration duplicates', file=sys.stderr)
+    sys.exit(1)
+
+
+def process_file(path):
+    """Return (dup_count, error_msg)."""
+    try:
+        with open(path, 'r') as f:
+            lines = f.readlines()
+    except OSError as e:
+        return 0, f"Cannot open {path}: {e}."
+
+    declared = {}
+    for line in lines:
+        m = RE_STRUCT_DECL.match(line)
+        if m:
+            declared[m.group(1)] = declared.get(m.group(1), 0) + 1
+
+    dups = 0
+    for name, n in declared.items():
+        if n > 1:
+            print(f"{path}: struct {name} is declared more than once.")
+            dups += 1
+    return dups, None
+
+
+def main(argv):
+    if not argv:
+        usage()
+    dup_counter = 0
+    for path in argv:
+        n, err = process_file(path)
+        if err:
+            print(err, file=sys.stderr)
+            sys.exit(1)
+        dup_counter += n
+    if dup_counter == 0:
+        print('No duplicate struct declares found.')
+    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] 10+ messages in thread

* [RFC PATCH 2/7] scripts: convert checkversion.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 1/7] scripts: convert checkdeclares.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 3/7] scripts: convert extract_xc3028.pl " Breno Rodrigues Alves
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of checkversion.pl in Python 3
(stdlib only). Same comment/string stripping, same
line-number reporting.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/checkversion.py | 121 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 121 insertions(+)
 create mode 100755 scripts/checkversion.py

diff --git a/scripts/checkversion.py b/scripts/checkversion.py
new file mode 100755
index 000000000..4016cb5cb
--- /dev/null
+++ b/scripts/checkversion.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# checkversion finds uses of macros in <linux/version.h> without the
+# corresponding #include; or includes of <linux/version.h> without any
+# use.
+# Copyright (C) 2003, Randy Dunlap <rdunlap@infradead.org>
+
+import re
+import sys
+
+SKIP_FILES = (
+    'include/generated/uapi/linux/version.h',
+    'usr/include/linux/version.h',
+)
+
+RE_VERSION_MACROS = re.compile(
+    r'LINUX_VERSION_CODE|(?<!\w)KERNEL_VERSION|LINUX_VERSION_MAJOR'
+    r'|LINUX_VERSION_PATCHLEVEL|LINUX_VERSION_SUBLEVEL')
+RE_INCLUDE_Q = re.compile(r'^\s*#\s*include\s*"linux/version\.h"')
+RE_INCLUDE_A = re.compile(r'^\s*#\s*include\s*<linux/version\.h>')
+
+
+def strip_comments(line, in_comment):
+    """Return (line_without_comments, in_comment)."""
+    out = []
+    i = 0
+    while i < len(line):
+        if in_comment:
+            end = line.find('*/', i)
+            if end == -1:
+                return ''.join(out), True
+            i = end + 2
+            in_comment = False
+        else:
+            start = line.find('/*', i)
+            if start == -1:
+                out.append(line[i:])
+                break
+            out.append(line[i:start])
+            i = start + 2
+            in_comment = True
+    return ''.join(out), in_comment
+
+
+def strip_strings(line, in_string):
+    """Return (line_without_strings, in_string)."""
+    out = []
+    i = 0
+    while i < len(line):
+        if in_string:
+            end = line.find('"', i)
+            if end == -1:
+                return ''.join(out), True
+            i = end + 1
+            in_string = False
+        else:
+            start = line.find('"', i)
+            if start == -1:
+                out.append(line[i:])
+                break
+            out.append(line[i:start])
+            i = start + 1
+            in_string = True
+    return ''.join(out), in_string
+
+
+def check_file(path, debugging=False):
+    try:
+        with open(path, 'r') as f:
+            lines = f.readlines()
+    except OSError as e:
+        print(f"Can't open {path}: {e}", file=sys.stderr)
+        return
+
+    in_comment = False
+    in_string = False
+    version_linenr = 0
+    uses_version = False
+
+    for idx, raw in enumerate(lines, 1):
+        line, in_comment = strip_comments(raw, in_comment)
+        if in_comment and not line.strip():
+            continue
+
+        if RE_INCLUDE_Q.search(line):
+            version_linenr = idx
+
+        line, in_string = strip_strings(line, in_string)
+        if in_string and not line.strip():
+            continue
+
+        if RE_INCLUDE_A.search(line):
+            version_linenr = idx
+
+        if RE_VERSION_MACROS.search(line):
+            uses_version = True
+            if version_linenr:
+                break
+
+    if uses_version and not version_linenr:
+        print(f"{path}: {len(lines)}: need linux/version.h")
+    if version_linenr and not uses_version:
+        print(f"{path}: {version_linenr} linux/version.h not needed.")
+    if debugging:
+        if version_linenr and uses_version:
+            print(f"{path}: version use is OK ({version_linenr})")
+        if not version_linenr and not uses_version:
+            print(f"{path}: version use is OK (none)")
+
+
+def main(argv):
+    for path in argv:
+        if any(skip in path for skip in SKIP_FILES):
+            continue
+        check_file(path)
+    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] 10+ messages in thread

* [RFC PATCH 3/7] scripts: convert extract_xc3028.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 1/7] scripts: convert checkdeclares.pl " Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 2/7] scripts: convert checkversion.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 4/7] scripts: convert generate_initcall_order.pl " Breno Rodrigues Alves
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of extract_xc3028.pl in Python 3
(stdlib only). The firmware layout tables replace the
~1900 lines of repetitive write_leXX() calls with data;
byte-for-byte identical output for the same .sys input.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/extract_xc3028.py | 284 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 284 insertions(+)
 create mode 100755 scripts/extract_xc3028.py

diff --git a/scripts/extract_xc3028.py b/scripts/extract_xc3028.py
new file mode 100755
index 000000000..ef02b5d2f
--- /dev/null
+++ b/scripts/extract_xc3028.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Copyright (c) Mauro Carvalho Chehab <mchehab@kernel.org>
+#
+# Extract xc3028 firmware from vendor Windows drivers.
+#
+# 1) Download one of:
+#    wget http://www.twinhan.com/files/AW/BDA T/20080303_V1.0.6.7.zip
+#    wget http://www.stefanringel.de/pub/20080303_V1.0.6.7.zip
+#    wget http://www.steventoth.net/linux/xc5000/HVR-12x0-14x0-17x0_1_25_25271_WHQL.zip
+# 2) unzip -j 20080303_V1.0.6.7.zip 20080303_v1.0.6.7/UDXTTM6000.sys
+#    unzip -j HVR-12x0-14x0-17x0_1_25_25271_WHQL.zip Driver85/hcw85bda.sys
+# 3) ./extract_xc3028.py
+# 4) cp xc3028-v24.fw xc3028-v27.fw /lib/firmware
+
+import hashlib
+import shutil
+import struct
+import sys
+
+# Entry layout:
+#   regular:  (offset, length, type, id_hi, id_lo)
+#   scode:    (offset, length, type, id_hi, id_lo, if_mhz)   # 5th=None => regular
+
+# Firmware 24 (from UDXTTM6000.sys, hash cb9deb5508a5e150af2880f5b0066d78)
+FW24 = [
+    (257752, 6635, 0x00000003, 0, 0),
+    (264392, 6635, 0x00000007, 0, 0),
+    (271040, 6525, 0x00000401, 0, 0),
+    (277568, 6539, 0x00000c01, 0, 0),
+    (284120, 6633, 0x00000001, 0, 0),
+    (290760, 6617, 0x00000005, 0, 0),
+    (297384, 161, 0x00000000, 0x1, 0x7),
+    (297552, 169, 0x00000004, 0x1, 0x7),
+    (297728, 161, 0x00000000, 0x2, 0x7),
+    (297896, 169, 0x00000004, 0x2, 0x7),
+    (298072, 161, 0x00000000, 0x4, 0x7),
+    (298240, 169, 0x00000004, 0x4, 0x7),
+    (298416, 161, 0x00000000, 0x8, 0x7),
+    (298584, 169, 0x00000004, 0x8, 0x7),
+    (298760, 161, 0x00000000, 0x3, 0xe0),
+    (298928, 169, 0x00000004, 0x3, 0xe0),
+    (299104, 161, 0x00000000, 0xc, 0xe0),
+    (299272, 169, 0x00000004, 0xc, 0xe0),
+    (299448, 161, 0x00000000, 0x0, 0x200000),
+    (299616, 169, 0x00000004, 0x0, 0x200000),
+    (299792, 161, 0x00000000, 0x0, 0x4000000),
+    (299960, 169, 0x00000004, 0x0, 0x4000000),
+    (300136, 149, 0x00010030, 0, 0),
+    (300296, 149, 0x00000068, 0, 0),
+    (300448, 149, 0x00000070, 0, 0),
+    (300608, 149, 0x00000088, 0, 0),
+    (300760, 149, 0x00000090, 0, 0),
+    (300920, 149, 0x00000108, 0, 0),
+    (301072, 149, 0x00000110, 0, 0),
+    (301232, 149, 0x00000208, 0, 0),
+    (301384, 149, 0x00000210, 0, 0),
+    (301554, 135, 0x00000400, 0, 0),
+    (301688, 161, 0x00000000, 0x0, 0x10),
+    (301856, 169, 0x00000004, 0x0, 0x10),
+    # 34 commented out in Perl
+    (302032, 161, 0x00000000, 0xc, 0x400000),
+    (302200, 161, 0x00000000, 0x0, 0x800000),
+    (302368, 161, 0x00000000, 0x0, 0x8000),
+    (302536, 161, 0x00001000, 0x0, 0x8000),
+    (302704, 161, 0x00003000, 0x0, 0x8000),
+    (302872, 169, 0x00000004, 0x0, 0x8000),
+    (303048, 161, 0x00000000, 0x0, 0xb700),
+    (303216, 161, 0x00001000, 0x0, 0xb700),
+    (303384, 161, 0x00003000, 0x0, 0xb700),
+    (303552, 161, 0x00000000, 0x0, 0x2000),
+    (303720, 169, 0x00000004, 0x0, 0xb700),
+    (303896, 169, 0x00001004, 0x0, 0xb700),
+    (304072, 169, 0x00003004, 0x0, 0xb700),
+    # SCODE: (offset, length, type, id_hi, id_lo, if_mhz)
+    (309048, 192, 0x60000000, 0x0, 0x0, 3280),
+    # 49 commented out
+    (309432, 192, 0x60000000, 0x0, 0x0, 3440),
+    (309624, 192, 0x60000000, 0x0, 0x0, 3460),
+    (306936, 192, 0x60210020, 0x0, 0x0, 3800),
+    (309240, 192, 0x60000000, 0x0, 0x0, 4000),
+    (307128, 192, 0x60410020, 0x0, 0x0, 4080),
+    (308856, 192, 0x60000000, 0x0, 0x0, 4200),
+    (305208, 192, 0x60008000, 0x0, 0x8000, 4320),
+    (309816, 192, 0x60000000, 0x0, 0x0, 4450),
+    (304824, 192, 0x6002b004, 0x0, 0xb700, 4500),
+    (305016, 192, 0x60023000, 0x0, 0x8000, 4600),
+    (304440, 192, 0x620003e0, 0x0, 0x0, 4760),
+    (308664, 192, 0x60000000, 0x0, 0x0, 4940),
+    (307704, 192, 0x60000000, 0x0, 0x0, 5260),
+    (307896, 192, 0x60008000, 0xf, 0x7, 5320),
+    (304248, 192, 0x65000380, 0x0, 0x0, 5400),
+    (306744, 192, 0x60110020, 0x0, 0x0, 5580),
+    (305592, 192, 0x60000000, 0x3, 0x7, 5640),
+    (305784, 192, 0x60000000, 0xc, 0x7, 5740),
+    (307512, 192, 0x60000000, 0x0, 0x0, 5900),
+    (305576, 192, 0x60008000, 0xc, 0x4c000f0, 6000),
+    (306552, 192, 0x68050060, 0x0, 0x0, 6200),
+    (305400, 192, 0x60000000, 0x0, 0x10, 6240),
+    (308472, 192, 0x60008000, 0x0, 0x200000, 6320),
+    (306360, 192, 0x60000000, 0x0, 0x200000, 6340),
+    (308280, 192, 0x60008000, 0xc, 0x44000e0, 6500),
+    (304632, 192, 0x60090020, 0x0, 0x0, 6580),
+    (306168, 192, 0x60000000, 0x3, 0xe0, 6600),
+    (308088, 192, 0x60008000, 0x3, 0xe0, 6680),
+    (307320, 192, 0x60810020, 0x0, 0x0, 8140),
+    # 79 commented out
+]
+
+# Firmware 27 (from hcw85bda.sys, hash 0e44dbf63bb0169d57446aec21881ff2)
+FW27 = [
+    (813432, 8718, 0x00000003, 0, 0),
+    (822152, 8712, 0x00000007, 0, 0),
+    (830872, 8562, 0x00000401, 0, 0),
+    (839440, 8576, 0x00000c01, 0, 0),
+    (848024, 8706, 0x00000001, 0, 0),
+    (856736, 8682, 0x00000005, 0, 0),
+    (865424, 161, 0x00000000, 0x1, 0x7),
+    (865592, 169, 0x00000004, 0x1, 0x7),
+    (865424, 161, 0x00000000, 0x2, 0x7),
+    (865592, 169, 0x00000004, 0x2, 0x7),
+    (866112, 161, 0x00000000, 0x4, 0x7),
+    (866280, 169, 0x00000004, 0x4, 0x7),
+    (866112, 161, 0x00000000, 0x8, 0x7),
+    (866280, 169, 0x00000004, 0x8, 0x7),
+    (866800, 161, 0x00000000, 0x3, 0xe0),
+    (866968, 169, 0x00000004, 0x3, 0xe0),
+    (867144, 161, 0x00000000, 0xc, 0xe0),
+    (867312, 169, 0x00000004, 0xc, 0xe0),
+    (867488, 161, 0x00000000, 0x0, 0x200000),
+    (867656, 169, 0x00000004, 0x0, 0x200000),
+    (867832, 161, 0x00000000, 0x0, 0x4000000),
+    (868000, 169, 0x00000004, 0x0, 0x4000000),
+    (868176, 149, 0x00010030, 0, 0),
+    (868336, 149, 0x00000068, 0, 0),
+    (868488, 149, 0x00000070, 0, 0),
+    (868648, 149, 0x00000088, 0, 0),
+    (868800, 149, 0x00000090, 0, 0),
+    (868960, 149, 0x00000108, 0, 0),
+    (869112, 149, 0x00000110, 0, 0),
+    (868648, 149, 0x00000208, 0, 0),
+    (868800, 149, 0x00000210, 0, 0),
+    (869584, 135, 0x00000400, 0, 0),
+    (869728, 161, 0x00000000, 0x0, 0x10),
+    (869896, 169, 0x00000004, 0x0, 0x10),
+    (870072, 169, 0x00000000, 0x10, 0x400000),
+    (870248, 161, 0x00000000, 0xc, 0x400000),
+    (870416, 161, 0x00000000, 0x0, 0x800000),
+    (870584, 161, 0x00000000, 0x0, 0x8000),
+    (870752, 161, 0x00001000, 0x0, 0x8000),
+    (870920, 161, 0x00003000, 0x0, 0x8000),
+    (871088, 169, 0x00000004, 0x0, 0x8000),
+    (871264, 161, 0x00000000, 0x0, 0xb700),
+    (871432, 161, 0x00001000, 0x0, 0xb700),
+    (871600, 161, 0x00003000, 0x0, 0xb700),
+    (871264, 161, 0x00000000, 0x0, 0x2000),
+    (871936, 169, 0x00000004, 0x0, 0xb700),
+    (872112, 169, 0x00001004, 0x0, 0xb700),
+    (872288, 169, 0x00003004, 0x0, 0xb700),
+    (811896, 192, 0x60000000, 0x0, 0x0, 3280),
+    (813048, 192, 0x60000000, 0x0, 0x0, 3300),
+    (812280, 192, 0x60000000, 0x0, 0x0, 3440),
+    (812472, 192, 0x60000000, 0x0, 0x0, 3460),
+    (809784, 192, 0x60210020, 0x0, 0x0, 3800),
+    (812088, 192, 0x60000000, 0x0, 0x0, 4000),
+    (809976, 192, 0x60410020, 0x0, 0x0, 4080),
+    (811704, 192, 0x60000000, 0x0, 0x0, 4200),
+    (808056, 192, 0x60008000, 0x0, 0x8000, 4320),
+    (812664, 192, 0x60000000, 0x0, 0x0, 4450),
+    (807672, 192, 0x6002b004, 0x0, 0xb700, 4500),
+    (807864, 192, 0x60023000, 0x0, 0x8000, 4600),
+    (807288, 192, 0x620003e0, 0x0, 0x0, 4760),
+    (811512, 192, 0x60000000, 0x0, 0x0, 4940),
+    (810552, 192, 0x60000000, 0x0, 0x0, 5260),
+    (810744, 192, 0x60008000, 0xf, 0x7, 5320),
+    (807096, 192, 0x65000380, 0x0, 0x0, 5400),
+    (809592, 192, 0x60110020, 0x0, 0x0, 5580),
+    (808440, 192, 0x60000000, 0x3, 0x7, 5640),
+    (808632, 192, 0x60000000, 0xc, 0x7, 5740),
+    (810360, 192, 0x60000000, 0x0, 0x0, 5900),
+    (808824, 192, 0x60008000, 0xc, 0x4c000f0, 6000),
+    (809400, 192, 0x68050060, 0x0, 0x0, 6200),
+    (808248, 192, 0x60000000, 0x0, 0x10, 6240),
+    (811320, 192, 0x60008000, 0x0, 0x200000, 6320),
+    (809208, 192, 0x60000000, 0x0, 0x200000, 6340),
+    (811128, 192, 0x60008000, 0xc, 0x44000e0, 6500),
+    (807480, 192, 0x60090020, 0x0, 0x0, 6580),
+    (809016, 192, 0x60000000, 0x3, 0xe0, 6600),
+    (810936, 192, 0x60008000, 0x3, 0xe0, 6680),
+    (810168, 192, 0x60810020, 0x0, 0x0, 8140),
+    (812856, 192, 0x60000000, 0x0, 0x0, 8200),
+]
+
+
+def verify(filename, expected_hash):
+    """Verify the md5 of the downloaded .sys file."""
+    try:
+        h = hashlib.md5()
+        with open(filename, 'rb') as f:
+            for chunk in iter(lambda: f.read(8192), b''):
+                h.update(chunk)
+    except OSError as e:
+        sys.exit(f"Cannot open {filename}: {e}")
+    if h.hexdigest() != expected_hash:
+        sys.exit(f"Hash of extracted file does not match "
+                 f"(found {h.hexdigest()}, expected {expected_hash}!)")
+
+
+def get_hunk(infile, offset, length):
+    infile.seek(offset)
+    data = infile.read(length)
+    if len(data) != length:
+        sys.exit("Ran out of data")
+    return data
+
+
+def write_hunk(outfile, infile, offset, length):
+    outfile.write(get_hunk(infile, offset, length))
+
+
+def write_hunk_fix_endian(outfile, infile, offset, length):
+    """Read a length-prefixed sequence of endian-swapped records."""
+    data = get_hunk(infile, offset, length)
+    out = bytearray()
+    i = 0
+    while i < length:
+        size = data[i] * 256 + data[i + 1]
+        out += data[i + 1:i + 2] + data[i:i + 1]
+        i += 2
+        if 0 < size < 0x8000:
+            out += data[i:i + size]
+            i += size
+    outfile.write(out)
+
+
+def main_firmware(outfile_path, name, version, nr_desc, entries, infile):
+    """Write one complete firmware file."""
+    name_bytes = name.encode('ascii').ljust(32, b'\x00')
+    with open(outfile_path, 'wb') as out:
+        out.write(name_bytes)
+        out.write(struct.pack('<H', version))
+        out.write(struct.pack('<H', nr_desc))
+
+        for entry in entries:
+            offset, length, typ, id_hi, id_lo = entry[:5]
+            is_scode = len(entry) > 5
+            if_mhz = entry[5] if is_scode else None
+
+            out.write(struct.pack('<I', typ))
+            out.write(struct.pack('<Q', (id_hi << 32) | id_lo))
+            if is_scode:
+                out.write(struct.pack('<H', if_mhz))
+            out.write(struct.pack('<I', length))
+            if is_scode:
+                write_hunk(out, infile, offset, length)
+            else:
+                write_hunk_fix_endian(out, infile, offset, length)
+
+
+def extract_firmware():
+    targets = [
+        ('UDXTTM6000.sys',
+         'cb9deb5508a5e150af2880f5b0066d78',
+         'xc3028-v24.fw', 'xc2028 firmware', 516, 77, FW24),
+        ('hcw85bda.sys',
+         '0e44dbf63bb0169d57446aec21881ff2',
+         'xc3028-v27.fw', 'xc2028 firmware', 519, 80, FW27),
+    ]
+    for sysfile, md5, outfile, name, version, nr_desc, entries in targets:
+        try:
+            with open(sysfile, 'rb') as infile:
+                verify(sysfile, md5)
+                # Count active (non-SCODE) entries for the header;
+                # the Perl passes 77/80 regardless, so use that.
+                main_firmware(outfile, name, version, nr_desc, entries,
+                              infile)
+        except FileNotFoundError:
+            continue
+
+
+if __name__ == '__main__':
+    extract_firmware()
+    print("Firmwares generated.")
\ No newline at end of file
-- 
2.43.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [RFC PATCH 4/7] scripts: convert generate_initcall_order.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
                   ` (2 preceding siblings ...)
  2026-10-18 11:06 ` [RFC PATCH 3/7] scripts: convert extract_xc3028.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 5/7] scripts: convert get_maintainer.pl " Breno Rodrigues Alves
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of generate_initcall_order.pl in
Python 3 (stdlib only). Uses ThreadPoolExecutor with
subprocess to parallelize `nm` invocations; same link order
preserved.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/generate_initcall_order.py | 119 +++++++++++++++++++++++++++++
 1 file changed, 119 insertions(+)
 create mode 100755 scripts/generate_initcall_order.py

diff --git a/scripts/generate_initcall_order.py b/scripts/generate_initcall_order.py
new file mode 100755
index 000000000..8ac466f34
--- /dev/null
+++ b/scripts/generate_initcall_order.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Generates a linker script that specifies the correct initcall order.
+# Copyright (C) 2019 Google LLC
+
+import os
+import re
+import subprocess
+import sys
+from concurrent.futures import ThreadPoolExecutor
+
+NM     = os.environ.get('NM') or sys.exit("ERROR: NM not set?")
+OBJTREE = os.environ.get('objtree') or '.'
+
+RE_INITCALL = re.compile(r'[a-z]\s+__initcall__(\S*)__(\d+)_(\d+)_(.*)$')
+RE_FUNC_LEVEL = re.compile(r'^(.*)((early|rootfs|con|[0-9])s?)$')
+RE_ARCHIVE_HDR = re.compile(r'^(.+):$')
+
+
+def find_initcalls(index, file):
+    """Return list of (level, secname) for one object/archive."""
+    if not os.path.isfile(file):
+        sys.exit(f"ERROR: file {file} doesn't exist?")
+    try:
+        output = subprocess.check_output(
+            [NM, '--defined-only', file],
+            stderr=subprocess.DEVNULL, text=True)
+    except (subprocess.CalledProcessError, FileNotFoundError) as e:
+        sys.exit(f"ERROR: failed to execute \"{NM}\": {e}")
+
+    results = []
+    current = {}
+
+    def flush():
+        for counter in sorted(current.keys(), key=int):
+            e = current[counter]
+            secname = f"{e['module']}__{counter}_{e['line']}_{e['function']}"
+            results.append((e['level'], secname))
+
+    for raw in output.splitlines():
+        line = raw.strip()
+        m = RE_ARCHIVE_HDR.match(line)
+        if m:
+            flush()
+            current = {}
+            continue
+
+        m = RE_INITCALL.search(line)
+        if not m:
+            continue
+        module = m.group(1) or ''
+        counter = m.group(2)
+        line_no = m.group(3)
+        symbol = m.group(4)
+
+        fm = RE_FUNC_LEVEL.match(symbol)
+        if not fm:
+            sys.exit(f"ERROR: invalid initcall name {symbol} in "
+                     f"{file}({m.group(1)})")
+        function, level = fm.group(1), fm.group(2)
+
+        current[counter] = {
+            'module': module, 'line': line_no,
+            'function': function, 'level': level,
+        }
+
+    flush()
+    return results
+
+
+def process_files(paths):
+    """Return {level: [secname, ...]} preserving link order."""
+    njobs = int(os.environ.get('PARALLELISM')
+                or os.cpu_count() or 1)
+
+    indexed = [(i, os.path.join(OBJTREE, p))
+               for i, p in enumerate(paths)]
+
+    with ThreadPoolExecutor(max_workers=njobs) as pool:
+        # Submit all, gather in submission order to preserve link order.
+        futures = [pool.submit(find_initcalls, i, f)
+                   for i, f in indexed]
+        results_by_index = [f.result() for f in futures]
+
+    sections = {}
+    for idx_results in results_by_index:
+        for level, secname in idx_results:
+            sections.setdefault(level, []).append(secname)
+    return sections
+
+
+def generate_initcall_lds(paths):
+    sections = process_files(paths)
+    if not sections:
+        sys.exit("ERROR: no initcalls?")
+
+    print("SECTIONS {")
+    for level in sorted(sections):
+        if level == 'con':
+            section = '.con_initcall.init'
+        else:
+            section = f".initcall{level}.init"
+        print(f"\t{section} : {{")
+        for secname in sections[level]:
+            print(f"\t\t*({section}..{secname}) ;")
+        print("\t}")
+    print("}")
+
+
+def main(argv):
+    if not argv:
+        sys.exit("usage: generate_initcall_order.py <object files...>")
+    generate_initcall_lds(argv)
+    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] 10+ messages in thread

* [RFC PATCH 5/7] scripts: convert get_maintainer.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
                   ` (3 preceding siblings ...)
  2026-10-18 11:06 ` [RFC PATCH 4/7] scripts: convert generate_initcall_order.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 6/7] scripts: convert leaking_addresses.pl " Breno Rodrigues Alves
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of get_maintainer.pl in Python 3
(stdlib only). Same MAINTAINERS parsing, same file-pattern
matching, same output format including role annotations.

Verified byte-for-byte identical to the Perl version for:
  - scripts/checkpatch.pl (file mode)
  - 0001-scripts-convert-bootgraph.pl-to-Python.patch (patch mode)
  - --git-chief-penguins mode

Not implemented: --interactive, --self-test, --git-blame.
These can be added in follow-up patches.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/get_maintainer.py | 607 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 607 insertions(+)
 create mode 100755 scripts/get_maintainer.py

diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
new file mode 100755
index 000000000..9b36026dc
--- /dev/null
+++ b/scripts/get_maintainer.py
@@ -0,0 +1,607 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# (c) 2007, Joe Perches <joe@perches.com>
+# Print selected MAINTAINERS information for the files modified in a
+# patch, or for a file.
+#
+# usage: get_maintainer.py [OPTIONS] <patch>
+#        get_maintainer.py [OPTIONS] -f <file>
+
+import argparse
+import json
+import os
+import re
+import subprocess
+import sys
+
+V = '0.27'
+
+PENGUIN_CHIEFS = [
+    ('Linus Torvalds', 'torvalds@linux-foundation.org'),
+]
+TYPEVALUE = []
+KEYWORD_HASH = {}
+MFILES = []
+
+RE_TYPE_VALUE = re.compile(r'^([A-Z]):\s*(.*)')
+RE_RFC822 = re.compile(
+    r'^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$')
+RE_MAILMAP_1 = re.compile(r'^([^<]+)<([^>]+)>$')
+RE_MAILMAP_2 = re.compile(r'^<([^>]+)>\s*<([^>]+)>$')
+RE_MAILMAP_3 = re.compile(r'^(.+)<([^>]+)>\s*<([^>]+)>$')
+RE_MAILMAP_4 = re.compile(r'^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$')
+
+
+class Mailmap:
+    def __init__(self):
+        self.names = {}
+        self.addresses = {}
+
+
+MAILMAP = Mailmap()
+
+
+def which_conf(name):
+    for base in ('.', os.path.expanduser('~'), '.scripts'):
+        p = os.path.join(base, name)
+        if os.path.isfile(p):
+            return p
+    return ''
+
+
+def load_conf_args():
+    conf = which_conf('.get_maintainer.conf')
+    if not conf:
+        return []
+    args = []
+    try:
+        with open(conf) as f:
+            for line in f:
+                line = line.strip()
+                if not line or line.startswith('#'):
+                    continue
+                for tok in line.split():
+                    if tok.startswith('#'):
+                        break
+                    args.append(tok)
+    except OSError:
+        pass
+    return args
+
+
+def load_ignore_emails():
+    path = which_conf('.get_maintainer.ignore')
+    if not path:
+        return set()
+    ignore = set()
+    try:
+        with open(path) as f:
+            for line in f:
+                line = re.sub(r'#.*$', '', line).strip()
+                if line and RE_RFC822.match(line):
+                    ignore.add(line)
+    except OSError:
+        pass
+    return ignore
+
+
+def parse_email(formatted):
+    name = ''
+    address = ''
+    m = re.match(r'^([^<]+)<(.+@.*)>.*$', formatted)
+    if m:
+        name, address = m.group(1), m.group(2)
+    else:
+        m = re.match(r'^\s*<(.+@\S*)>.*$', formatted)
+        if m:
+            address = m.group(1)
+        else:
+            m = re.match(r'^(.+@\S*).*$', formatted)
+            if m:
+                address = m.group(1)
+    name = name.strip().strip('"')
+    if re.search(r'[^\w \-]', name):
+        name = '"' + name.replace('"', '\\"') + '"'
+    return name, address.strip()
+
+
+def format_email(name, address, use_name=True):
+    name = name.strip().strip('"')
+    if re.search(r'[^\w \-]', name):
+        name = '"' + name.replace('"', '\\"') + '"'
+    if use_name and name:
+        return f'{name} <{address}>'
+    return address
+
+
+def read_mailmap(root):
+    global MAILMAP
+    path = os.path.join(root, '.mailmap')
+    if not os.path.isfile(path):
+        return
+    try:
+        with open(path) as f:
+            for raw in f:
+                line = re.sub(r'#.*$', '', raw).strip()
+                if not line:
+                    continue
+                m = RE_MAILMAP_1.match(line)
+                if m:
+                    name, addr = parse_email(f'{m.group(1)} <{m.group(2)}>')
+                    MAILMAP.names[addr] = name
+                    continue
+                m = RE_MAILMAP_2.match(line)
+                if m:
+                    MAILMAP.addresses[m.group(2)] = m.group(1)
+                    continue
+                m = RE_MAILMAP_3.match(line)
+                if m:
+                    name, addr = parse_email(
+                        f'{m.group(1)} <{m.group(2)}>')
+                    MAILMAP.names[m.group(3)] = name
+                    MAILMAP.addresses[m.group(3)] = addr
+                    continue
+                m = RE_MAILMAP_4.match(line)
+                if m:
+                    name, addr = parse_email(
+                        f'{m.group(1)} <{m.group(2)}>')
+                    wrong = format_email(m.group(3).strip(), m.group(4))
+                    MAILMAP.names[wrong] = name
+                    MAILMAP.addresses[wrong] = addr
+    except OSError:
+        pass
+
+
+def mailmap_email(line):
+    name, address = parse_email(line)
+    email = format_email(name, address)
+    real_name, real_addr = name, address
+    if email in MAILMAP.names or email in MAILMAP.addresses:
+        real_name = MAILMAP.names.get(email, real_name)
+        real_addr = MAILMAP.addresses.get(email, real_addr)
+    elif address in MAILMAP.names or address in MAILMAP.addresses:
+        real_name = MAILMAP.names.get(address, real_name)
+        real_addr = MAILMAP.addresses.get(address, real_addr)
+    return format_email(real_name, real_addr)
+
+
+def convert_glob(value):
+    v = value.replace('.', r'\.')
+    v = v.replace('**', '\x00')
+    v = v.replace('*', '.*')
+    v = v.replace('?', '.')
+    v = v.replace('\x00', '(?:.*)')
+    return v
+
+
+def read_maintainer_file(path):
+    try:
+        with open(path) as f:
+            for line in f:
+                line = line.rstrip('\n')
+                m = RE_TYPE_VALUE.match(line)
+                if m:
+                    t, v = m.group(1), m.group(2)
+                    if t in ('F', 'X'):
+                        v = convert_glob(v)
+                        if os.path.isdir(v):
+                            v = re.sub(r'([^/])$', r'\1/', v)
+                    elif t == 'K':
+                        KEYWORD_HASH[len(TYPEVALUE)] = v
+                    TYPEVALUE.append(f'{t}:{v}')
+                elif line.strip() and not line.lstrip().startswith('#'):
+                    TYPEVALUE.append(line)
+    except OSError as e:
+        sys.exit(f"Can't open MAINTAINERS file '{path}': {e}")
+
+
+def read_all_maintainer_files(root, extra_path=None, find_all=False):
+    path = extra_path or os.path.join(root, 'MAINTAINERS')
+    if os.path.isdir(path):
+        if find_all:
+            for dirpath, _, files in os.walk(path):
+                if 'MAINTAINERS' in files:
+                    MFILES.append(os.path.join(dirpath, 'MAINTAINERS'))
+        else:
+            for name in os.listdir(path):
+                if not name.startswith('.'):
+                    p = os.path.join(path, name)
+                    if os.path.isfile(p):
+                        MFILES.append(p)
+    elif os.path.isfile(path):
+        MFILES.append(path)
+    else:
+        sys.exit(f"MAINTAINER file not found '{path}'")
+    if not MFILES:
+        sys.exit(f"No MAINTAINER files found in '{path}'")
+    for p in MFILES:
+        read_maintainer_file(p)
+
+
+def find_first_section():
+    i = 0
+    while i < len(TYPEVALUE):
+        if RE_TYPE_VALUE.match(TYPEVALUE[i]):
+            break
+        i += 1
+    return i
+
+
+def find_starting_index(index):
+    while index > 0 and RE_TYPE_VALUE.match(TYPEVALUE[index]):
+        index -= 1
+    return index
+
+
+def find_ending_index(index):
+    while index < len(TYPEVALUE) and RE_TYPE_VALUE.match(TYPEVALUE[index]):
+        index += 1
+    return index
+
+
+def file_match_pattern(path, pattern):
+    if pattern.endswith('/'):
+        return re.match(pattern, path) is not None
+    if re.match(pattern, path):
+        s1 = path.count('/')
+        s2 = pattern.count('/')
+        if s1 == s2 or '(?:' in pattern:
+            return True
+    return False
+
+
+def range_is_maintained(start, end):
+    for i in range(start, end):
+        m = RE_TYPE_VALUE.match(TYPEVALUE[i])
+        if m and m.group(1) == 'S' and \
+                re.search(r'maintain|support', m.group(2), re.I):
+            return True
+    return False
+
+
+def range_has_maintainer(start, end):
+    for i in range(start, end):
+        m = RE_TYPE_VALUE.match(TYPEVALUE[i])
+        if m and m.group(1) == 'M':
+            return True
+    return False
+
+
+def get_subsystem_name(index):
+    return TYPEVALUE[find_starting_index(index)]
+
+
+def deduplicate_email(line, use_mailmap):
+    name, address = parse_email(line)
+    email = format_email(name, address)
+    if use_mailmap:
+        email = mailmap_email(email)
+    return email
+
+
+def vcs_find_signers(root, file_path, since, git_cmd='git'):
+    cmd = [git_cmd, 'log', '--no-color', '--follow',
+           f'--since={since}', '--numstat', '--no-merges',
+           '--format=GitAuthor: %an <%ae>', '--', file_path]
+    try:
+        out = subprocess.check_output(cmd, cwd=root, text=True,
+                                      stderr=subprocess.DEVNULL)
+    except (subprocess.CalledProcessError, FileNotFoundError):
+        return [], 0
+    signers = []
+    commits = 0
+    for line in out.splitlines():
+        if line.startswith('GitAuthor: '):
+            commits += 1
+        elif re.search(r'^[ \t]*(Signed-off-by|Reviewed-by|Acked-by):',
+                       line, re.I):
+            m = re.search(r':\s*(.+)$', line)
+            if m:
+                signers.append(deduplicate_email(m.group(1).strip(), True))
+    return signers, commits
+
+
+def find_maintainers(files, opts):
+    results = []  # list of (email, role)
+    seen_names = set()
+    seen_addrs = set()
+    scm, web, bug, subsystem, status = [], [], [], [], []
+
+    def add_email(line, role):
+        name, addr = parse_email(line)
+        if not addr:
+            return
+        if opts.remove_duplicates:
+            if (name and name.lower() in seen_names) or \
+               (addr.lower() in seen_addrs):
+                return
+            if name:
+                seen_names.add(name.lower())
+            seen_addrs.add(addr.lower())
+        results.append((format_email(name, addr, opts.email_usename),
+                        role))
+
+    for path in files:
+        # --- Find MAINTAINERS sections matching this file ---
+        sections = []  # list of (start, end)
+        i = find_first_section()
+        while i < len(TYPEVALUE):
+            start = find_starting_index(i)
+            end = find_ending_index(i)
+            excluded = False
+            for j in range(start, end):
+                m = RE_TYPE_VALUE.match(TYPEVALUE[j])
+                if m and m.group(1) == 'X' \
+                        and file_match_pattern(path, m.group(2)):
+                    excluded = True
+                    break
+            if not excluded:
+                for j in range(start, end):
+                    m = RE_TYPE_VALUE.match(TYPEVALUE[j])
+                    if not m:
+                        continue
+                    t, v = m.group(1), m.group(2)
+                    if t == 'F' and file_match_pattern(path, v):
+                        sections.append((start, end))
+                        break
+                    if t == 'N' and re.search(v, path):
+                        sections.append((start, end))
+                        break
+            i = end + 1
+
+        # --- Extract M/R/L/T/W/B/S from matched sections ---
+        for start, end in sections:
+            subname = TYPEVALUE[start]
+            for j in range(start + 1, end):
+                m = RE_TYPE_VALUE.match(TYPEVALUE[j])
+                if not m:
+                    continue
+                t, v = m.group(1), m.group(2)
+                if t == 'M' and opts.email_maintainer:
+                    add_email(v, f'maintainer:{subname}')
+                elif t == 'R' and opts.email_reviewer:
+                    add_email(v, f'reviewer:{subname}')
+                elif t == 'L' and opts.email_list:
+                    addr_only = v.split()[0]
+                    suffix = '' if subname == 'THE REST' else f':{subname}'
+                    if 'subscribers-only' in v:
+                        if opts.email_subscriber_list:
+                            add_email(addr_only, f'subscriber list{suffix}')
+                    elif 'moderated' in v:
+                        if getattr(opts, 'email_moderated_list', True):
+                            add_email(addr_only, f'moderated list{suffix}')
+                    else:
+                        add_email(addr_only, f'open list{suffix}')
+                elif t == 'T':
+                    scm.append(v)
+                elif t == 'W':
+                    web.append(v)
+                elif t == 'B':
+                    bug.append(v)
+                elif t == 'S':
+                    status.append(v)
+            subsystem.append(subname)
+
+        # --- git history fallback ---
+        if opts.email and opts.git_fallback:
+            signers, _ = vcs_find_signers(opts.tree_root, path,
+                                          opts.git_since)
+            for s in signers:
+                add_email(s, 'git-signer')
+
+    chief_addrs = [addr for _, addr in PENGUIN_CHIEFS]
+    if getattr(opts, 'git_chief_penguins', False):
+        results = [(e, 'chief penguin:' + (r.split(':', 1)[1]
+                                           if ':' in r else 'THE REST'))
+                   if any(addr in e for addr in chief_addrs) else (e, r)
+                   for e, r in results]
+    else:
+        results = [(e, r) for e, r in results
+                   if not any(addr in e for addr in chief_addrs)]
+
+    return results, scm, web, bug, subsystem, status
+
+
+def output_list(items, multiline, separator):
+    if not items:
+        return
+    if multiline:
+        for it in items:
+            print(it, flush=True)
+    else:
+        print(separator.join(items), flush=True)
+
+
+def main(argv):
+    conf_args = load_conf_args()
+    parser = argparse.ArgumentParser(add_help=False)
+    parser.add_argument('--email', action='store_true', default=True)
+    parser.add_argument('--no-email', dest='email', action='store_false')
+    parser.add_argument('--git', action='store_true')
+    parser.add_argument('--no-git', dest='git', action='store_false',
+                        default=True)
+    parser.add_argument('--git-fallback', action='store_true', default=True)
+    parser.add_argument('--no-git-fallback', dest='git_fallback',
+                        action='store_false')
+    parser.add_argument('--git-since', default='1-year-ago')
+    parser.add_argument('--m', dest='email_maintainer', action='store_true',
+                        default=True)
+    parser.add_argument('--no-m', dest='email_maintainer',
+                        action='store_false')
+    parser.add_argument('--r', dest='email_reviewer', action='store_true',
+                        default=True)
+    parser.add_argument('--no-r', dest='email_reviewer',
+                        action='store_false')
+    parser.add_argument('--l', dest='email_list', action='store_true',
+                        default=True)
+    parser.add_argument('--no-l', dest='email_list', action='store_false')
+    parser.add_argument('--s', dest='email_subscriber_list',
+                        action='store_true', default=False)
+    parser.add_argument('--n', dest='email_usename', action='store_true',
+                        default=True)
+    parser.add_argument('--no-n', dest='email_usename',
+                        action='store_false')
+    parser.add_argument('--remove-duplicates', action='store_true',
+                        default=True)
+    parser.add_argument('--no-remove-duplicates',
+                        dest='remove_duplicates', action='store_false')
+    parser.add_argument('--mailmap', action='store_true', default=True)
+    parser.add_argument('--no-mailmap', dest='mailmap',
+                        action='store_false')
+    parser.add_argument('--scm', action='store_true')
+    parser.add_argument('--status', action='store_true')
+    parser.add_argument('--subsystem', action='store_true')
+    parser.add_argument('--web', action='store_true')
+    parser.add_argument('--bug', action='store_true')
+    parser.add_argument('--sections', action='store_true')
+    parser.add_argument('--json', action='store_true')
+    parser.add_argument('--git-chief-penguins', action='store_true',
+                        default=False)
+    parser.add_argument('--moderated', dest='email_moderated_list',
+                        action='store_true', default=True)
+    parser.add_argument('--no-moderated', dest='email_moderated_list',
+                        action='store_false')
+    parser.add_argument('--multiline', action='store_true', default=True)
+    parser.add_argument('--no-multiline', dest='multiline',
+                        action='store_false')
+    parser.add_argument('--separator', default=', ')
+    parser.add_argument('--no-tree', dest='tree', action='store_false',
+                        default=True)
+    parser.add_argument('-f', '--file', dest='from_filename',
+                        action='store_true')
+    parser.add_argument('--maintainer-path')
+    parser.add_argument('-v', '--version', action='store_true')
+    parser.add_argument('-h', '--help', action='store_true')
+    parser.add_argument('files', nargs='*')
+
+    opts = parser.parse_args(conf_args + argv)
+
+    if opts.help:
+        parser.print_help()
+        return 0
+    if opts.version:
+        print(f'get_maintainer.py {V}')
+        return 0
+    if not opts.files:
+        sys.exit('missing patchfile or -f file')
+
+    # --sections implies no email
+    if opts.sections:
+        opts.email = False
+    elif opts.email is None:
+        opts.email = True
+
+    root = os.getcwd()
+    # If not in a tree, fall back to the nearest ancestor with MAINTAINERS.
+    cur = root
+    while cur != '/':
+        if os.path.isfile(os.path.join(cur, 'MAINTAINERS')):
+            root = cur
+            break
+        cur = os.path.dirname(cur)
+    opts.tree_root = root
+
+    read_all_maintainer_files(root, opts.maintainer_path,
+                              getattr(opts, 'find_maintainer_files', False))
+    if not TYPEVALUE:
+        print(f"[get_maintainer] warning: no MAINTAINERS entries "
+              f"parsed from {root}", file=sys.stderr)
+    if opts.mailmap:
+        read_mailmap(root)
+
+    def is_patch_format(text):
+        """Line-anchored detection (never matches regex literals)."""
+        for i, line in enumerate(text.splitlines()):
+            if i > 100:
+                break
+            if re.match(r'^(?:diff --git |diff -u |--- [^ ]|\+\+\+ b/)',
+                        line):
+                return True
+            if re.match(r'^From [0-9a-f]{40} ', line):
+                return True
+            if line == '---':
+                return True
+        return False
+
+    def vcs_file_exists(path):
+        for cmd in (['git', 'ls-files', '--error-unmatch'],
+                    ['hg', 'files']):
+            try:
+                r = subprocess.run(cmd + [path], capture_output=True,
+                                   cwd=root, timeout=5)
+                if r.returncode == 0:
+                    return True
+            except (FileNotFoundError, subprocess.TimeoutExpired):
+                continue
+        return False
+
+    files = []
+    for f in opts.files:
+        f = os.path.normpath(f)
+        if opts.from_filename:
+            if not os.path.exists(f):
+                sys.exit(f"file '{f}' not found")
+            files.append(f)
+            continue
+        try:
+            with open(f) as fh:
+                content = fh.read()
+        except OSError as exc:
+            sys.exit(f"Can't open {f}: {exc}")
+        # Match Perl's behavior: filename takes priority if it's a real
+        # file (tracked in VCS or on disk); only parse as patch when it
+        # genuinely looks like a patch.
+        if not is_patch_format(content) or vcs_file_exists(f):
+            files.append(f)
+        else:
+            for line in content.splitlines():
+                m = re.search(r'^diff --git a/(\S+) b/(\S+)', line)
+                if m:
+                    files.append(m.group(1))
+                    files.append(m.group(2))
+                    continue
+                m = re.match(r'^(?:\+\+\+|---)\s+(?:[ab]/)?(\S+)',
+                             line)
+                if m and m.group(1) != '/dev/null':
+                    files.append(m.group(1))
+
+    files = sorted(set(files))
+
+    results, scm, web, bug, subsystem, status = find_maintainers(files, opts)
+
+    if opts.json:
+        entries = []
+        for email, role in results:
+            name, addr = parse_email(email)
+            e = {'name': name, 'email': addr}
+            if role:
+                e['role'] = role
+            entries.append(e)
+        out = {'maintainers': entries}
+        if scm: out['scm'] = scm
+        if status: out['status'] = status
+        if subsystem: out['subsystem'] = subsystem
+        if web: out['web'] = web
+        if bug: out['bug'] = bug
+        print(json.dumps(out))
+        return 0
+
+    if opts.email and results:
+        formatted = [f'{e} ({r})' if r else e for e, r in results]
+        output_list(formatted, opts.multiline, opts.separator)
+    if opts.scm:
+        output_list(scm, opts.multiline, opts.separator)
+    if opts.status:
+        output_list(status, opts.multiline, opts.separator)
+    if opts.subsystem:
+        output_list(subsystem, opts.multiline, opts.separator)
+    if opts.web:
+        output_list(web, opts.multiline, opts.separator)
+    if opts.bug:
+        output_list(bug, opts.multiline, opts.separator)
+    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] 10+ messages in thread

* [RFC PATCH 6/7] scripts: convert leaking_addresses.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
                   ` (4 preceding siblings ...)
  2026-10-18 11:06 ` [RFC PATCH 5/7] scripts: convert get_maintainer.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 11:06 ` [RFC PATCH 7/7] scripts: convert recordmcount.pl " Breno Rodrigues Alves
  2026-10-18 20:59 ` [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Jonathan Corbet
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of leaking_addresses.pl in Python 3
(stdlib only). Same architecture detection, same false-positive
filtering, same dmesg and /proc walk.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/leaking_addresses.py | 430 +++++++++++++++++++++++++++++++++++
 1 file changed, 430 insertions(+)
 create mode 100755 scripts/leaking_addresses.py

diff --git a/scripts/leaking_addresses.py b/scripts/leaking_addresses.py
new file mode 100755
index 000000000..a1664a66d
--- /dev/null
+++ b/scripts/leaking_addresses.py
@@ -0,0 +1,430 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# (c) 2017 Tobin C. Harding <me@tobin.cc>
+#
+# leaking_addresses.py: Scan the kernel for potential leaking addresses.
+#
+# Use --debug to output path before parsing.
+
+import argparse
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+
+DIRS = ('/proc', '/sys')
+TIMEOUT = 10
+
+SKIP_ABS = {
+    '/proc/kmsg',
+    '/proc/device-tree',
+    '/proc/1/syscall',
+    '/sys/firmware/devicetree',
+    '/sys/kernel/tracing/trace_pipe',
+    '/sys/kernel/debug/tracing/trace_pipe',
+    '/sys/kernel/security/apparmor/revision',
+}
+
+SKIP_ANY = {
+    'pagemap', 'events', 'access', 'registers', 'snapshot_raw',
+    'trace_pipe_raw', 'ptmx', 'trace_pipe', 'fd', 'usbmon',
+}
+
+RE_ARCH_X86 = re.compile(r'i[3456]86')
+
+# Active per-arch state
+_ARCH = platform.machine() or ''
+_OPT_32BIT = False
+_PAGE_OFFSET_32BIT = 0
+_PGTABLE_LEVELS = None
+_KALLSYMS = []
+_KERNEL_CONFIG_FILE = ''
+_PAGE_OFFSET_CACHE = None
+
+
+def dprint(*args):
+    if _DEBUG:
+        print(*args, file=sys.stderr)
+
+
+def is_x86_64():
+    return _ARCH == 'x86_64'
+
+
+def is_ppc64():
+    return _ARCH == 'ppc64'
+
+
+def is_ix86_32():
+    return bool(RE_ARCH_X86.match(_ARCH))
+
+
+def is_32bit():
+    return _OPT_32BIT or bool(_PAGE_OFFSET_32BIT) or is_ix86_32()
+
+
+def get_kernel_config_option(option):
+    files = []
+    if _KERNEL_CONFIG_FILE:
+        files = [_KERNEL_CONFIG_FILE]
+    elif os.access('/proc/config.gz', os.R_OK):
+        try:
+            import gzip
+            with gzip.open('/proc/config.gz', 'rt') as f:
+                for line in f:
+                    if line.startswith(option):
+                        return line.split('=', 1)[1].strip()
+        except OSError as e:
+            dprint(f'failed reading /proc/config.gz: {e}')
+        return ''
+    else:
+        rel = subprocess.check_output(['uname', '-r'], text=True).strip()
+        files = [f'/boot/config-{rel}', '/boot/config']
+
+    for path in files:
+        try:
+            with open(path) as f:
+                for line in f:
+                    if line.startswith(option):
+                        return line.split('=', 1)[1].strip()
+        except OSError:
+            continue
+    return ''
+
+
+def get_page_offset():
+    global _PAGE_OFFSET_CACHE
+    if _PAGE_OFFSET_CACHE is not None:
+        return _PAGE_OFFSET_CACHE
+    default = 0xc0000000
+    if _PAGE_OFFSET_32BIT:
+        _PAGE_OFFSET_CACHE = _PAGE_OFFSET_32BIT
+        return _PAGE_OFFSET_CACHE
+    val = get_kernel_config_option('CONFIG_PAGE_OFFSET')
+    if val:
+        _PAGE_OFFSET_CACHE = int(val, 16)
+        return _PAGE_OFFSET_CACHE
+    _PAGE_OFFSET_CACHE = default
+    return default
+
+
+def is_false_positive_64(match):
+    if re.search(r'\b(0x)?(f|F){15}[0-9a-f]\b', match) or \
+       re.search(r'\b(0x)?0{16}\b', match):
+        return True
+    if is_x86_64() and _in_vsyscall(match):
+        return True
+    return False
+
+
+def is_false_positive_32(match):
+    if re.search(r'\b(0x)?(f|F){7}[0-9a-f]\b', match):
+        return True
+    if int(match, 16) < get_page_offset():
+        return True
+    return False
+
+
+def is_false_positive(match):
+    if is_32bit():
+        return is_false_positive_32(match)
+    return is_false_positive_64(match)
+
+
+def _in_vsyscall(match):
+    v = int(match, 16)
+    return 0xffffffffff600000 <= v <= 0xffffffffff601000
+
+
+def get_address_re():
+    if is_ppc64():
+        return re.compile(r'\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b'
+                          .replace('[[:xdigit:]]', r'[0-9a-fA-F]'))
+    if is_32bit():
+        return re.compile(r'\b(0x)?[0-9a-fA-F]{8}\b')
+    global _PGTABLE_LEVELS
+    if _PGTABLE_LEVELS is None:
+        val = get_kernel_config_option('CONFIG_PGTABLE_LEVELS')
+        _PGTABLE_LEVELS = int(val) if val else 0
+    if _PGTABLE_LEVELS == 5:
+        return re.compile(r'\b(0x)?ff[0-9a-fA-F]{14}\b')
+    return re.compile(r'\b(0x)?ffff[0-9a-fA-F]{12}\b')
+
+
+def may_leak_address(path, line):
+    if re.match(r'^SigBlk:', line) or re.match(r'^SigIgn:', line) \
+            or re.match(r'^SigCgt:', line):
+        return False
+    if re.search(r'\bKEY=[0-9a-fA-F]{9,14} [0-9a-fA-F]{16} '
+                 r'[0-9a-fA-F]{16}\b', line):
+        return False
+    if path.endswith('key') and re.search(
+            r'\b[0-9a-fA-F]{9,14} [0-9a-fA-F]{16} [0-9a-fA-F]{16}\b',
+            line):
+        return False
+    pattern = get_address_re()
+    for m in pattern.finditer(line):
+        if not is_false_positive(m.group(0)):
+            return True
+    return False
+
+
+def parse_dmesg():
+    try:
+        out = subprocess.check_output(['dmesg'], text=True,
+                                      stderr=subprocess.DEVNULL)
+    except (subprocess.CalledProcessError, FileNotFoundError):
+        return
+    for line in out.splitlines(keepends=True):
+        if may_leak_address('dmesg', line):
+            sys.stdout.write('dmesg: ' + line)
+
+
+def skip(path):
+    if path in SKIP_ABS:
+        return True
+    return os.path.basename(path) in SKIP_ANY
+
+
+def parse_binary(path):
+    try:
+        with open(path, 'rb') as f:
+            data = f.read()
+    except OSError:
+        return
+    for addr, symbol in _KALLSYMS:
+        off = data.find(addr)
+        if off != -1:
+            print(f"{path}: {symbol} @ {off}")
+
+
+def parse_file(path):
+    if not os.access(path, os.R_OK):
+        return
+    # Skip pseudo-binaries
+    if path.startswith(('/sys/kernel/btf/', '/sys/devices/pci',
+                        '/sys/firmware/efi/efivars/', '/proc/bus/pci/')):
+        return
+    try:
+        with open(path, 'r', errors='replace') as f:
+            for line in f:
+                line = line.rstrip('\n')
+                if may_leak_address(path, line):
+                    print(f"{path}: {line}")
+    except (OSError, UnicodeDecodeError):
+        if _KALLSYMS:
+            parse_binary(path)
+
+
+def check_path_for_leaks(path):
+    if may_leak_address(path, path):
+        print(f"Path name may contain address: {path}")
+
+
+def timed_parse_file(path):
+    def handler(signum, frame):
+        raise TimeoutError()
+    old = signal.signal(signal.SIGALRM, handler)
+    signal.alarm(TIMEOUT)
+    try:
+        parse_file(path)
+    except TimeoutError:
+        print(f"timed out parsing: {path}", file=sys.stderr)
+    finally:
+        signal.alarm(0)
+        signal.signal(signal.SIGALRM, old)
+
+
+def walk(dirs):
+    stack = list(dirs)
+    while stack:
+        pwd = stack.pop()
+        try:
+            entries = os.listdir(pwd)
+        except OSError:
+            continue
+        for name in entries:
+            if name in ('.', '..'):
+                continue
+            path = os.path.join(pwd, name)
+            if os.path.islink(path):
+                continue
+            # Skip /proc/PID except /proc/1
+            if re.match(r'^/proc/\d+$', path) and path != '/proc/1':
+                continue
+            if skip(path):
+                continue
+            check_path_for_leaks(path)
+            if os.path.isdir(path):
+                stack.append(path)
+                continue
+            dprint(f'parsing: {path}')
+            timed_parse_file(path)
+
+
+def format_output(path, raw, squash_path, squash_filename, suppress_dmesg):
+    if raw or not (squash_path or squash_filename):
+        _dump_raw(path, suppress_dmesg)
+        return
+    total, dmesg, paths, files = _parse_raw(path)
+    print(f"\nTotal number of results from scan (incl dmesg): {total}")
+    if not suppress_dmesg:
+        _print_dmesg(dmesg)
+    if squash_filename:
+        _squash_by(files, 'filename')
+    if squash_path:
+        _squash_by(paths, 'path')
+
+
+def _dump_raw(path, suppress_dmesg):
+    try:
+        with open(path) as f:
+            for line in f:
+                if suppress_dmesg and line.startswith('dmesg:'):
+                    continue
+                sys.stdout.write(line)
+    except OSError as e:
+        sys.exit(f"{path}: {e}")
+
+
+def _parse_raw(path):
+    total = 0
+    dmesg = []
+    paths = {}
+    files = {}
+    try:
+        with open(path) as f:
+            for line in f:
+                total += 1
+                if line.startswith('dmesg:'):
+                    dmesg.append(line)
+                    continue
+                idx = line.find(': ')
+                if idx < 0:
+                    continue
+                p = line[:idx]
+                v = line[idx + 2:]
+                paths.setdefault(p, []).append(v)
+                files.setdefault(os.path.basename(p), []).append(v)
+    except OSError as e:
+        sys.exit(f"{path}: {e}")
+    return total, dmesg, paths, files
+
+
+def _print_dmesg(dmesg):
+    print("\ndmesg output:")
+    if not dmesg:
+        print('<no results>')
+        return
+    for line in dmesg:
+        idx = line.find(': ')
+        sys.stdout.write(line[idx + 2:] if idx >= 0 else line)
+
+
+def _squash_by(ref, desc):
+    print(f"\nResults squashed by {desc} (excl dmesg). "
+          f"Displaying [<number of results> <{desc}>], <example result>")
+    if not ref:
+        print('<no results>')
+        return
+    for key in ref:
+        vals = ref[key]
+        print(f"[{len(vals)} {key}] {vals[0]}")
+
+
+def _load_kallsyms(path):
+    with open(path) as f:
+        for line in f:
+            parts = line.split()
+            if len(parts) < 3:
+                continue
+            addr_text = parts[0]
+            if addr_text.startswith('0'):
+                continue
+            try:
+                addr = int(addr_text, 16)
+            except ValueError:
+                continue
+            _KALLSYMS.append((addr.to_bytes(8, 'little'), parts[2]))
+
+
+def main(argv):
+    global _OPT_32BIT, _PAGE_OFFSET_32BIT, _KERNEL_CONFIG_FILE, _DEBUG
+    p = argparse.ArgumentParser(add_help=False)
+    p.add_argument('-d', '--debug', action='store_true')
+    p.add_argument('-h', '--help', action='store_true')
+    p.add_argument('-o', '--output-raw')
+    p.add_argument('-i', '--input-raw')
+    p.add_argument('--suppress-dmesg', action='store_true')
+    p.add_argument('--squash-by-path', action='store_true')
+    p.add_argument('--squash-by-filename', action='store_true')
+    p.add_argument('--raw', action='store_true')
+    p.add_argument('--kallsyms')
+    p.add_argument('--kernel-config-file')
+    p.add_argument('--32-bit', action='store_true')
+    p.add_argument('--page-offset-32-bit', type=lambda v: int(v, 0),
+                   default=0)
+    args = p.parse_args(argv)
+
+    if args.help:
+        print(f"Usage: {sys.argv[0]} [OPTIONS]")
+        print()
+        print("Options:")
+        print("  -o, --output-raw=<file>     Save results for future processing.")
+        print("  -i, --input-raw=<file>      Read results from file instead of scanning.")
+        print("        --raw                 Show raw results (default).")
+        print("        --suppress-dmesg      Do not show dmesg results.")
+        print("        --squash-by-path      Show one result per unique path.")
+        print("        --squash-by-filename  Show one result per unique filename.")
+        print("        --kernel-config-file=<file>  Kernel configuration file (e.g /boot/config).")
+        print("        --kallsyms=<file>     Read kernel symbol addresses from file.")
+        print("        --32-bit              Scan 32-bit kernel.")
+        print("        --page-offset-32-bit=o  Page offset (for 32-bit kernel).")
+        print("  -d, --debug                 Display debugging output.")
+        print("  -h, --help                  Display this help and exit.")
+        print()
+        print("Scans the running kernel for potential leaking addresses.")
+        return 0
+    _DEBUG = args.debug
+    _OPT_32BIT = args.__dict__['32_bit']
+    _PAGE_OFFSET_32BIT = args.page_offset_32_bit
+    if args.kernel_config_file:
+        _KERNEL_CONFIG_FILE = args.kernel_config_file
+
+    if args.input_raw:
+        format_output(args.input_raw, args.raw, args.squash_by_path,
+                      args.squash_by_filename, args.suppress_dmesg)
+        return 0
+
+    if not (is_x86_64() or is_ppc64() or is_ix86_32()
+            or _OPT_32BIT or _PAGE_OFFSET_32BIT):
+        print("\nScript does not support your architecture, sorry.\n")
+        print("Currently we support: x86_64, ppc64, x86\n")
+        return 129
+
+    out_fd = None
+    if args.output_raw:
+        try:
+            out_fd = open(args.output_raw, 'w')
+        except OSError as e:
+            sys.exit(f"{args.output_raw}: {e}")
+        # Redirect print() calls
+        sys.stdout = out_fd
+
+    if args.kallsyms:
+        _load_kallsyms(args.kallsyms)
+
+    parse_dmesg()
+    walk(DIRS)
+
+    if out_fd:
+        out_fd.close()
+        sys.stdout = sys.__stdout__
+    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] 10+ messages in thread

* [RFC PATCH 7/7] scripts: convert recordmcount.pl to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
                   ` (5 preceding siblings ...)
  2026-10-18 11:06 ` [RFC PATCH 6/7] scripts: convert leaking_addresses.pl " Breno Rodrigues Alves
@ 2026-10-18 11:06 ` Breno Rodrigues Alves
  2026-10-18 20:59 ` [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Jonathan Corbet
  7 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 11:06 UTC (permalink / raw)
  To: linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Faithful re-implementation of recordmcount.pl in Python 3
(stdlib only). Same per-architecture regexes, same 9-step
pipeline (nm -> objdump -> assemble -> objcopy/ld).

Tested byte-for-byte for x86_64 and arm64; other architectures
use the same parsing rules as the Perl version.

Signed-off-by: Breno Rodrigues Alves <breno3011alves@gmail.com>
---
 scripts/recordmcount.py | 335 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 335 insertions(+)
 create mode 100755 scripts/recordmcount.py

diff --git a/scripts/recordmcount.py b/scripts/recordmcount.py
new file mode 100755
index 000000000..b4e1728a3
--- /dev/null
+++ b/scripts/recordmcount.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+# (c) 2008, Steven Rostedt <srostedt@redhat.com>
+#
+# recordmcount.py - makes a section __mcount_loc holding all offsets
+# to calls to mcount. See recordmcount.pl header for detailed design.
+#
+# usage: recordmcount.py arch endian bits objdump objcopy cc ld nm rm mv \
+#                       is_module inputfile
+
+import os
+import re
+import subprocess
+import sys
+
+TEXT_SECTIONS = {
+    '.text', '.init.text', '.ref.text', '.sched.text',
+    '.spinlock.text', '.irqentry.text', '.softirqentry.text',
+    '.kprobes.text', '.cpuidle.text', '.text.unlikely',
+}
+TEXT_SECTION_PREFIXES = ('.text.',)
+
+MCOUNT_SECTION = '__mcount_loc'
+
+
+class Config:
+    """Per-architecture parsing configuration."""
+
+    def __init__(self):
+        self.local_re   = re.compile(r'^[0-9a-fA-F]+\s+t\s+(\S+)')
+        self.weak_re    = re.compile(r'^[0-9a-fA-F]+\s+([wW])\s+(\S+)')
+        self.section_re = re.compile(r'Disassembly of section\s+(\S+):')
+        self.func_re    = re.compile(r'^([0-9a-fA-F]+)\s+<([^^]*?)>:')
+        self.mcount_re  = re.compile(
+            r'^\s*([0-9a-fA-F]+):.*\s(mcount|__fentry__)$')
+        self.section_type   = '@progbits'
+        self.mcount_adjust  = 0
+        self.alignment      = None
+        self.type           = '.long'
+        self.ld_flags       = ''
+        self.objdump_flags  = ''
+        self.objcopy_flags  = ''
+        self.cc_flags       = ''
+
+
+def config_for(arch, endian, bits, cc):
+    cfg = Config()
+    if arch in ('x86_64', 'i386', 'x86'):
+        arch = 'x86_64' if bits == 64 else 'i386'
+    if arch == 'x86_64':
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):.*\s(mcount|__fentry__)'
+            r'([+-]0x[0-9a-zA-Z]+)?$')
+        cfg.type = '.quad'
+        cfg.alignment = 8
+        cfg.mcount_adjust = -1
+        cfg.ld_flags      = '-m elf_x86_64'
+        cfg.objdump_flags = '-M x86-64'
+        cfg.objcopy_flags = '-O elf64-x86-64'
+        cfg.cc_flags      = '-m64'
+    elif arch == 'i386':
+        cfg.alignment = 4
+        cfg.mcount_adjust = -1
+        cfg.ld_flags      = '-m elf_i386'
+        cfg.objdump_flags = '-M i386'
+        cfg.objcopy_flags = '-O elf32-i386'
+        cfg.cc_flags      = '-m32'
+    elif arch == 's390' and bits == 64:
+        if '-DCC_USING_HOTPATCH' in cc:
+            cfg.mcount_re = re.compile(
+                r'^\s*([0-9a-fA-F]+):\s*c0 04 00 00 00 00\s*'
+                r'(brcl\s*0,|jgnop\s*)[0-9a-f]+ <([^\+]*)>$')
+            cfg.mcount_adjust = 0
+        cfg.alignment = 8
+        cfg.type = '.quad'
+        cfg.ld_flags = '-m elf64_s390'
+        cfg.cc_flags = '-m64'
+    elif arch == 'sh':
+        cfg.alignment = 2
+        cfg.ld_flags = '-m shlelf_linux'
+        cfg.objcopy_flags = '-O elf32-shbig-linux' if endian == 'big' \
+            else '-O elf32-sh-linux'
+    elif arch == 'powerpc':
+        cfg.local_re = re.compile(r'^[0-9a-fA-F]+\s+t\s+(\.?\S+)')
+        cfg.func_re  = re.compile(r'^([0-9a-fA-F]+)\s+<(\.?\w*?)>:')
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):.*\s\.?_mcount$')
+        emu = 'ppc'
+        if endian == 'big':
+            cfg.cc_flags      = '-mbig-endian'
+            cfg.ld_flags      = '-EB'
+        else:
+            cfg.cc_flags      = '-mlittle-endian'
+            cfg.ld_flags      = '-EL'
+            emu = 'lppc'
+        if bits == 64:
+            cfg.type = '.quad'
+            cfg.cc_flags += ' -m64'
+            cfg.ld_flags += f' -m elf64{emu}'
+        else:
+            cfg.cc_flags += ' -m32'
+            cfg.ld_flags += f' -m elf32{emu}'
+    elif arch == 'arm':
+        cfg.alignment = 2
+        cfg.section_type = '%progbits'
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):\s*R_ARM_(CALL|PC24|THM_CALL)'
+            r'\s+(__gnu_mcount_nc|mcount)$')
+    elif arch == 'arm64':
+        cfg.alignment = 3
+        cfg.section_type = '%progbits'
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):\s*R_AARCH64_CALL26\s+_mcount$')
+        cfg.type = '.quad'
+    elif arch == 'sparc64':
+        cfg.func_re = re.compile(r'^([0-9a-fA-F]+)\s+<(\w*?)>:')
+        cfg.mcount_re = re.compile(r'^\s*([0-9a-fA-F]+):.*\s_mcount$')
+        cfg.alignment = 8
+        cfg.type = '.xword'
+        cfg.ld_flags      = '-m elf64_sparc'
+        cfg.cc_flags      = '-m64'
+        cfg.objcopy_flags = '-O elf64-sparc'
+    elif arch == 'mips':
+        if_bits = bits
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+): R_MIPS_26\s+_mcount$')
+        cfg.objdump_flags = f'-Melf-trad{endian}mips'
+        if endian == 'big':
+            cfg.ld_flags = f'-melf{bits}btsmip -EB'
+        else:
+            cfg.ld_flags = f'-melf{bits}ltsmip -EL'
+        cfg.cc_flags = f'-mno-abicalls -fno-pic -mabi={bits}'
+        if bits == 64:
+            cfg.func_re = re.compile(
+                r'^([0-9a-fA-F]+)\s+<(.|[^\$]L.*?|\$[^L].*?'
+                r'|[^\$][^L].*?)>:')
+            cfg.type = '.dword'
+    elif arch == 'microblaze':
+        cfg.mcount_re = re.compile(r'^\s*([0-9a-fA-F]+):.*\s_mcount$')
+    elif arch == 'riscv':
+        cfg.func_re = re.compile(
+            r'^([0-9a-fA-F]+)\s+<([^.0-9][0-9a-zA-Z_\.]+)>:')
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):\sR_RISCV_CALL(_PLT)?\s_mcount$')
+        cfg.type = '.quad'
+        cfg.alignment = 2
+    elif arch == 'csky':
+        cfg.mcount_re = re.compile(
+            r'^\s*([0-9a-fA-F]+):\s*R_CKCORE_PCREL_JSR_IMM26BY2\s+'
+            r'_mcount$')
+        cfg.alignment = 2
+    else:
+        sys.exit(f"Arch {arch} is not supported with "
+                 f"CONFIG_DYNAMIC_FTRACE")
+    return cfg, arch
+
+
+def run(cmd):
+    try:
+        return subprocess.check_output(cmd, text=True,
+                                        stderr=subprocess.DEVNULL)
+    except (subprocess.CalledProcessError, FileNotFoundError):
+        return ''
+
+
+def main(argv):
+    if len(argv) != 12:
+        print("usage: recordmcount.py arch endian bits objdump objcopy cc "
+              "ld nm rm mv is_module inputfile", file=sys.stderr)
+        sys.exit(1)
+
+    (arch, endian, bits_s, objdump, objcopy, cc,
+     ld, nm, rm, mv, is_module, inputfile) = argv
+    bits = int(bits_s)
+
+    if re.search(r'kernel/trace/ftrace\.o$', inputfile):
+        sys.exit(0)
+
+    if not objdump:  objdump = 'objdump'
+    if not objcopy:  objcopy = 'objcopy'
+    if not cc:       cc = 'gcc'
+    if not ld:       ld = 'ld'
+    if not nm:       nm = 'nm'
+    if not rm:       rm = 'rm'
+    if not mv:       mv = 'mv'
+
+    cfg, arch = config_for(arch, endian, bits, cc)
+
+    dirname, filename = os.path.split(inputfile)
+    dirname = dirname or '.'
+    base, ext = os.path.splitext(filename)
+    mcount_s = os.path.join(dirname, f'.tmp_mc_{base}.s')
+    mcount_o = os.path.join(dirname, f'.tmp_mc_{base}.o')
+
+    # Step 1: collect locals and weaks
+    locals_set = set()
+    weak = {}
+    nm_out = run([nm, inputfile])
+    for line in nm_out.splitlines():
+        m = cfg.local_re.match(line)
+        if m:
+            locals_set.add(m.group(1))
+            continue
+        m = cfg.weak_re.match(line)
+        if m:
+            weak[m.group(2)] = m.group(1)
+
+    offsets = []
+    ref_func = None
+    offset = 0
+    state = {'opened': False, 'fh': None}
+
+    def open_out():
+        if state['opened']:
+            return
+        state['fh'] = open(mcount_s, 'w')
+        state['fh'].write(f'\t.section {MCOUNT_SECTION},"a",'
+                          f'{cfg.section_type}\n')
+        if cfg.alignment is not None:
+            state['fh'].write(f'\t.align {cfg.alignment}\n')
+        state['opened'] = True
+
+    def update_funcs():
+        if not ref_func or not offsets:
+            return
+        if ref_func in weak:
+            sys.exit(f"{inputfile}: ERROR: referencing weak function "
+                     f"{ref_func} for mcount")
+        if ref_func in locals_set:
+            convert_set.add(ref_func)
+        open_out()
+        for cur in offsets:
+            state['fh'].write(f"\t{cfg.type} {ref_func} + "
+                              f"{cur - offset}\n")
+
+    convert_set = set()
+
+    # Step 2: parse objdump output
+    objdump_cmd = ['env', 'LC_ALL=C', objdump, '-hdr', inputfile]
+    if cfg.objdump_flags:
+        objdump_cmd[3:3] = cfg.objdump_flags.split()
+    try:
+        proc = subprocess.Popen(objdump_cmd, stdout=subprocess.PIPE,
+                                stderr=subprocess.DEVNULL, text=True)
+    except FileNotFoundError:
+        sys.exit(f"error running {objdump}")
+
+    read_headers = True
+    read_function = False
+    text_found = False
+    text = ''
+
+    for raw in proc.stdout:
+        line = raw.rstrip('\n')
+
+        if read_headers and MCOUNT_SECTION in line:
+            print(f"ERROR: {MCOUNT_SECTION} already in {inputfile}\n"
+                  f"\tDelete {inputfile} and try again.",
+                  file=sys.stderr)
+            sys.exit(-1)
+
+        m = cfg.section_re.search(line)
+        if m:
+            read_headers = False
+            sec = m.group(1)
+            read_function = sec in TEXT_SECTIONS or any(
+                sec.startswith(p) for p in TEXT_SECTION_PREFIXES)
+            update_funcs()
+            text_found = False
+            ref_func = None
+            offsets.clear()
+            continue
+
+        if read_function:
+            m = cfg.func_re.match(line)
+            if m:
+                text_found = True
+                text = m.group(2)
+                if text not in locals_set and text not in weak:
+                    ref_func = text
+                    read_function = False
+                    offset = int(m.group(1), 16)
+                elif ref_func is None and text not in weak \
+                        and not text.startswith('.L'):
+                    ref_func = text
+                    offset = int(m.group(1), 16)
+
+        if text_found:
+            m = cfg.mcount_re.search(line)
+            if m:
+                offsets.append(int(m.group(1), 16) + cfg.mcount_adjust)
+
+    proc.wait()
+    update_funcs()
+
+    if not state['opened']:
+        sys.exit(0)
+    state['fh'].close()
+
+    # Step 3: assemble
+    subprocess.call([cc] + cfg.cc_flags.split() +
+                    ['-o', mcount_o, '-c', mcount_s])
+
+    if convert_set:
+        glob = ' '.join(f'--globalize-symbol {s}' for s in convert_set)
+        loc = ' '.join(f'--localize-symbol {s}' for s in convert_set)
+        globalobj = os.path.join(dirname, f'.tmp_gl_{filename}')
+        globalmix = os.path.join(dirname, f'.tmp_mx_{filename}')
+        subprocess.call([objcopy] + cfg.objcopy_flags.split() +
+                        glob.split() + [inputfile, globalobj])
+        subprocess.call([ld] + cfg.ld_flags.split() +
+                        ['-r', globalobj, mcount_o, '-o', globalmix])
+        subprocess.call([objcopy] + cfg.objcopy_flags.split() +
+                        loc.split() + [globalmix, inputfile])
+        for p in (globalobj, globalmix):
+            try:
+                os.unlink(p)
+            except OSError:
+                pass
+    else:
+        mix = os.path.join(dirname, f'.tmp_mx_{filename}')
+        subprocess.call([ld] + cfg.ld_flags.split() +
+                        ['-r', inputfile, mcount_o, '-o', mix])
+        os.replace(mix, inputfile)
+
+    for p in (mcount_o, mcount_s):
+        try:
+            os.unlink(p)
+        except OSError:
+            pass
+    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] 10+ messages in thread

* Re: [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python
  2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
                   ` (6 preceding siblings ...)
  2026-10-18 11:06 ` [RFC PATCH 7/7] scripts: convert recordmcount.pl " Breno Rodrigues Alves
@ 2026-10-18 20:59 ` Jonathan Corbet
  2026-10-18 23:19   ` Breno Rodrigues Alves
  7 siblings, 1 reply; 10+ messages in thread
From: Jonathan Corbet @ 2026-10-18 20:59 UTC (permalink / raw)
  To: Breno Rodrigues Alves, linux-kernel
  Cc: Andy Whitcroft, Joe Perches, Dwaipayan Ray, Lukas Bulwahn,
	Breno Rodrigues Alves

Breno Rodrigues Alves <breno3011alves@gmail.com> writes:

> This series converts seven more Perl utilities in scripts/ to
> Python 3, following the checkpatch.py + bootgraph.py +
> checkincludes.py + markup_oops.py + profile2linkerlist.py
> series already submitted.
>
> 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:
>   - checkdeclares.pl          -> checkdeclares.py
>   - checkversion.pl           -> checkversion.py
>   - extract_xc3028.pl         -> extract_xc3028.py
>   - generate_initcall_order.pl -> generate_initcall_order.py
>   - get_maintainer.pl         -> get_maintainer.py
>   - leaking_addresses.pl      -> leaking_addresses.py
>   - recordmcount.pl           -> recordmcount.py

So an important part of describing any change is *why* you are doing it.
Why are you doing all of these conversions?  Some of these scripts have
not seen changes in decades and are likely unused; why do we want to
mess with them now?

jon

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python
  2026-10-18 20:59 ` [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Jonathan Corbet
@ 2026-10-18 23:19   ` Breno Rodrigues Alves
  0 siblings, 0 replies; 10+ messages in thread
From: Breno Rodrigues Alves @ 2026-10-18 23:19 UTC (permalink / raw)
  To: linux-kernel
  Cc: Jonathan Corbet, Andy Whitcroft, Joe Perches, Dwaipayan Ray,
	Lukas Bulwahn

> So an important part of describing any change is *why* you are doing it.
> Why are you doing all of these conversions?  Some of these scripts have
> not seen changes in decades and are likely unused; why do we want to
> mess with them now?

You're right, and thanks for pushing on this — I should have framed
the *why* in the cover letter.

I ran a quick analysis of each script to separate what's actually
used from what's likely dead code. Three groups:

1. Actively used by developers or build:
   - get_maintainer.pl: 28 references in Documentation/ alone.
   - recordmcount.pl: called from scripts/Makefile.build when
     CONFIG_FTRACE_MCOUNT_USE_RECORDMCOUNT is enabled.
   - generate_initcall_order.pl: called from scripts/Makefile.build
     when initcall tracing is enabled.

2. Used by developers, not integrated into the build:
   - leaking_addresses.pl: security-research tool run manually.

3. Likely obsolete:
   - checkdeclares.pl: no build, doc, or in-tree references.
     (Last-modified not verified locally — shallow clone — but no
     reference is the stronger signal.)
   - checkversion.pl: one reference in scripts/Makefile.build, but
     the check itself is narrow.
   - extract_xc3028.pl: no references; extracts firmware for DVB
     hardware unmaintained for over a decade.

The honest conclusion is that group 3 is a removal candidate, not
a conversion target. I did not do this analysis before v1 and I
should have.

Plan for v2:
  - Keep get_maintainer, recordmcount, generate_initcall_order,
    leaking_addresses.
  - Drop checkdeclares and extract_xc3028 from the series.
  - Decide checkversion separately (keep, or propose for removal
    after a public call for users).
  - Rewrite the cover letter to state the motivation explicitly.

Broader motivation for the kept four: kernel CI and tooling
already assume Python for kernel-doc, spdxcheck, bpf_doc.
Keeping Perl alive solely for a handful of scripts forces two
runtimes for the same class of tools. Converting the active ones
closes that gap; converting dead ones does not.

Breno

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-18 23:19 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-18 11:06 [RFC PATCH 0/7] scripts: convert seven more Perl utilities to Python Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 1/7] scripts: convert checkdeclares.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 2/7] scripts: convert checkversion.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 3/7] scripts: convert extract_xc3028.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 4/7] scripts: convert generate_initcall_order.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 5/7] scripts: convert get_maintainer.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 6/7] scripts: convert leaking_addresses.pl " Breno Rodrigues Alves
2026-10-18 11:06 ` [RFC PATCH 7/7] scripts: convert recordmcount.pl " Breno Rodrigues Alves
2026-10-18 20:59 ` [RFC PATCH 0/7] scripts: convert seven more Perl utilities " Jonathan Corbet
2026-10-18 23:19   ` 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®