mail archive of the barebox mailing list
 help / color / mirror / Atom feed
From: Ahmad Fatoum <a.fatoum@pengutronix.de>
To: barebox@lists.infradead.org
Cc: Ahmad Fatoum <a.fatoum@pengutronix.de>,
	"Claude Sonnet 4.5" <claude@anthropic.com>
Subject: [PATCH 4/5] Documentation: generate docs for magic variables
Date: Mon, 15 Dec 2025 08:59:00 +0100	[thread overview]
Message-ID: <20251215075907.1222551-4-a.fatoum@pengutronix.de> (raw)
In-Reply-To: <20251215075907.1222551-1-a.fatoum@pengutronix.de>

We have documentation for magic variables scattered around the docs, but
some variables docs is only available at runtime using the magicvar
command.

Let's include a listing of all extracted BAREBOX_MAGICVAR(..., ...)
occurrences in the code base formatted as a table in a hierarchical
manner.

The code is LLM-generated by feeding in the existing gen_commands.py
in addition to the prompt.

Co-developed-by: Claude Sonnet 4.5 <claude@anthropic.com>
Signed-off-by: Ahmad Fatoum <a.fatoum@pengutronix.de>
---
 Documentation/.gitignore         |   1 +
 Documentation/Makefile           |  11 +-
 Documentation/gen_magicvars.py   | 357 +++++++++++++++++++++++++++++++
 Documentation/user/variables.rst |  11 +-
 4 files changed, 372 insertions(+), 8 deletions(-)
 create mode 100755 Documentation/gen_magicvars.py

diff --git a/Documentation/.gitignore b/Documentation/.gitignore
index e8b2851d2552..4e35bebf8d51 100644
--- a/Documentation/.gitignore
+++ b/Documentation/.gitignore
@@ -2,4 +2,5 @@
 
 build
 commands
+magicvars.rst
 html
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 825cd3ba0e7f..03daa0a2ab2f 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -15,12 +15,17 @@ objtree ?= $(srctree)
 
 docs: htmldocs FORCE
 
-htmldocs: FORCE
-	@mkdir -p $(srctree)/Documentation/commands
-	@$(srctree)/Documentation/gen_commands.py $(srctree) $(srctree)/Documentation/commands
+htmldocs: gen_commands gen_magicvars FORCE
 	@$(SPHINXBUILD) -b html -d $(objtree)/doctrees $(srctree)/Documentation \
 		$(objtree)/Documentation/html $(ALLSPHINXOPTS)
 
+gen_commands: FORCE
+	@mkdir -p $(srctree)/Documentation/commands
+	@$(srctree)/Documentation/gen_commands.py $(srctree) $(srctree)/Documentation/commands
+
+gen_magicvars: FORCE
+	@$(srctree)/Documentation/gen_magicvars.py $(srctree) $(srctree)/Documentation/user/magicvars.rst
+
 dochelp: FORCE
 	@echo  ' barebox internal documentation from ReST:'
 	@echo  '  htmldocs        - HTML'
diff --git a/Documentation/gen_magicvars.py b/Documentation/gen_magicvars.py
new file mode 100755
index 000000000000..e2bb02036051
--- /dev/null
+++ b/Documentation/gen_magicvars.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+
+import os
+import re
+import sys
+import hashlib
+from collections import defaultdict
+
+MAGICVAR = re.compile(r'BAREBOX_MAGICVAR\s*\(\s*(\S+)\s*,\s*"(.*?)"\s*\)')
+MAGICVAR_START = re.compile(r'BAREBOX_MAGICVAR\s*\(\s*(\S+)\s*,\s*"(.*?)$')
+MAGICVAR_CONT = re.compile(r'(.*?)"\s*\)')
+
+UNESCAPE = re.compile(r'''\\(["\\])''')
+
+# Global override dictionary to map directories to specific arch or board
+DIRECTORY_OVERRIDES = {
+    'drivers/soc/kvx': {'arch': 'kvx'},
+}
+
+
+def string_unescape(s):
+    return re.sub(UNESCAPE, r'\1', s.replace(r'\t', '').replace(r'\n', ''))
+
+
+def parse_c(filepath):
+    """Parse a C file and extract BAREBOX_MAGICVAR declarations."""
+    magicvars = []
+
+    with open(filepath, 'r') as f:
+        content = f.read()
+
+    # Try to find all BAREBOX_MAGICVAR occurrences
+    # First, try single-line matches
+    for match in MAGICVAR.finditer(content):
+        varname = match.group(1).strip()
+        description = string_unescape(match.group(2))
+        magicvars.append((varname, description))
+
+    # For multi-line matches, we need to search line by line
+    lines = content.split('\n')
+    i = 0
+    while i < len(lines):
+        line = lines[i]
+
+        # Check if this line was already matched by single-line regex
+        if MAGICVAR.search(line):
+            i += 1
+            continue
+
+        # Try multi-line match
+        m = MAGICVAR_START.search(line)
+        if m:
+            varname = m.group(1).strip()
+            description = m.group(2)
+            i += 1
+
+            # Continue reading lines until we find the closing
+            while i < len(lines):
+                line = lines[i]
+                m_end = MAGICVAR_CONT.search(line)
+                if m_end:
+                    description += m_end.group(1)
+                    break
+                else:
+                    description += line
+                i += 1
+
+            description = string_unescape(description)
+            magicvars.append((varname, description))
+
+        i += 1
+
+    return magicvars
+
+
+def categorize_file(filepath):
+    """Determine if a file is arch-specific, board-specific, or common."""
+    parts = filepath.split(os.sep)
+
+    arch = None
+    board = None
+    
+    # Check for directory overrides first
+    for override_path, override_spec in DIRECTORY_OVERRIDES.items():
+        override_parts = override_path.split('/')
+        # Check if the filepath starts with the override path
+        if len(parts) >= len(override_parts):
+            if all(parts[i] == override_parts[i] for i in range(len(override_parts))):
+                # This file matches the override path
+                if 'arch' in override_spec:
+                    arch = override_spec['arch']
+                if 'board' in override_spec:
+                    board = override_spec['board']
+                return arch, board
+
+    # Check for arch
+    if 'arch' in parts:
+        arch_idx = parts.index('arch')
+        if arch_idx + 1 < len(parts):
+            arch = parts[arch_idx + 1]
+
+    # Check for board
+    if 'boards' in parts:
+        boards_idx = parts.index('boards')
+        if boards_idx + 1 < len(parts):
+            board = parts[boards_idx + 1]
+
+    return arch, board
+
+
+def get_prefix_group(varname):
+    """Get the prefix group for a variable name."""
+    if '.' not in varname:
+        return None  # No prefix group
+
+    parts = varname.split('.')
+    if len(parts) == 2:
+        return parts[0]  # e.g., "global" from "global.option"
+    else:
+        return '.'.join(parts[:-1])  # e.g., "global.bootm" from "global.bootm.option"
+
+
+def hoist_single_elements(grouped_vars):
+    """Hoist single-element groups to their parent group."""
+    # Find single-element groups
+    single_element_groups = []
+    for prefix, vars_list in list(grouped_vars.items()):
+        if prefix and len(vars_list) == 1:
+            single_element_groups.append(prefix)
+
+    # Hoist them
+    for prefix in single_element_groups:
+        varname, description = grouped_vars[prefix][0]
+        del grouped_vars[prefix]
+
+        # Find parent prefix
+        if '.' in prefix:
+            parent_prefix = '.'.join(prefix.split('.')[:-1])
+        else:
+            parent_prefix = None
+
+        grouped_vars[parent_prefix].append((varname, description))
+
+    return grouped_vars
+
+
+def generate_table(vars_list, arch=None, board=None):
+    """Generate a ReST table for a list of variables."""
+    if not vars_list:
+        return ""
+
+    # Sort alphabetically
+    vars_list = sorted(vars_list, key=lambda x: x[0])
+
+    lines = []
+    lines.append(".. list-table::")
+    lines.append("   :header-rows: 1")
+    lines.append("   :align: left")
+    lines.append("   :widths: 30 70")
+    lines.append("   :width: 100%")
+    lines.append("")
+    lines.append("   * - Variable")
+    lines.append("     - Description")
+
+    for varname, description in vars_list:
+        # Generate anchor
+        anchor = varname.replace('.', '_')
+        if board:
+            anchor = f"{anchor}_{board}"
+        elif arch:
+            anchor = f"{anchor}_{arch}"
+
+        lines.append(f"   * - .. _magicvar_{anchor}:")
+        lines.append("")
+        lines.append(f"       ``{varname}``")
+        lines.append(f"     - {description}")
+
+    return '\n'.join(lines)
+
+
+def generate_rst(common_vars, arch_vars, board_vars):
+    """Generate the complete ReST documentation."""
+    out = []
+
+    # Group common variables by prefix
+    grouped = defaultdict(list)
+    for varname, description in common_vars:
+        prefix = get_prefix_group(varname)
+        grouped[prefix].append((varname, description))
+
+    # Hoist single-element groups
+    grouped = hoist_single_elements(grouped)
+
+    # Build a hierarchy tree
+    hierarchy = defaultdict(lambda: {'vars': [], 'children': {}})
+
+    for prefix, vars_list in grouped.items():
+        if prefix is None:
+            hierarchy[None]['vars'] = vars_list
+        else:
+            # Split into top-level and sub-level
+            parts = prefix.split('.')
+            if len(parts) == 1:
+                # Top-level like "global"
+                hierarchy[prefix]['vars'] = vars_list
+            else:
+                # Sub-level like "global.bootm"
+                top_level = parts[0]
+                hierarchy[top_level]['children'][prefix] = vars_list
+
+    # Generate output with hierarchy
+    # First, handle "No prefix" group
+    if None in hierarchy and hierarchy[None]['vars']:
+        out.append("No prefix")
+        out.append("-" * len(out[-1]))
+        out.append("")
+        out.append(generate_table(hierarchy[None]['vars']))
+        out.append("")
+
+    # Then handle top-level groups in alphabetical order
+    sorted_top_level = sorted([k for k in hierarchy.keys() if k is not None])
+
+    for top_prefix in sorted_top_level:
+        group = hierarchy[top_prefix]
+
+        # Top-level heading
+        out.append(f"{top_prefix}.*")
+        out.append("-" * len(out[-1]))
+        out.append("")
+
+        # Top-level variables table (if any)
+        if group['vars']:
+            out.append(generate_table(group['vars']))
+            out.append("")
+
+        # Sub-level groups
+        for sub_prefix in sorted(group['children'].keys()):
+            out.append(f"{sub_prefix}.*")
+            out.append("^" * len(out[-1]))
+            out.append("")
+            out.append(generate_table(group['children'][sub_prefix]))
+            out.append("")
+
+    # Architecture-specific variables
+    if arch_vars:
+        for arch in sorted(arch_vars.keys()):
+            out.append(f"{arch}-specific variables")
+            out.append("-" * len(out[-1]))
+            out.append("")
+            out.append(generate_table(arch_vars[arch], arch=arch))
+            out.append("")
+
+    # Board-specific variables
+    if board_vars:
+        out.append("Board-specific variables")
+        out.append("-" * len(out[-1]))
+        out.append("")
+
+        for board in sorted(board_vars.keys()):
+            out.append(board)
+            out.append("^" * len(out[-1]))
+            out.append("")
+            out.append(generate_table(board_vars[board], board=board))
+            out.append("")
+
+    return '\n'.join(out)
+
+
+def main():
+    if len(sys.argv) != 3:
+        print("Usage: gen_magicvars.py <source_dir> <output_file>", file=sys.stderr)
+        sys.exit(1)
+
+    source_dir = sys.argv[1]
+    output_file = sys.argv[2]
+
+    # Collect all magic variables
+    all_vars = defaultdict(lambda: defaultdict(set))  # {varname: {description: set of (arch, board)}}
+
+    for root, dirs, files in os.walk(source_dir):
+        for name in files:
+            if name.endswith('.c'):
+                filepath = os.path.join(root, name)
+                relpath = os.path.relpath(filepath, source_dir)
+
+                arch, board = categorize_file(relpath)
+                magicvars = parse_c(filepath)
+
+                for varname, description in magicvars:
+                    all_vars[varname][description].add((arch, board))
+
+    # Check for inconsistent descriptions
+    for varname, descriptions in all_vars.items():
+        if len(descriptions) > 1:
+            print(f"gen_magicvars: warning: variable '{varname}' has multiple different descriptions", file=sys.stderr)
+            for desc in descriptions:
+                print(f"  - {desc}", file=sys.stderr)
+
+    # Organize variables into categories
+    common_vars = []
+    arch_vars = defaultdict(list)
+    board_vars = defaultdict(list)
+
+    for varname, descriptions in all_vars.items():
+        # Use the first description (or we could merge them)
+        description = list(descriptions.keys())[0]
+        locations = list(descriptions.values())[0]
+
+        # Determine where this variable should go
+        is_board = any(board is not None for arch, board in locations)
+        is_arch = any(arch is not None and board is None for arch, board in locations)
+        is_common = any(arch is None and board is None for arch, board in locations)
+
+        # Priority: board > arch > common
+        if is_board:
+            for arch, board in locations:
+                if board is not None:
+                    board_vars[board].append((varname, description))
+        elif is_arch:
+            for arch, board in locations:
+                if arch is not None and board is None:
+                    arch_vars[arch].append((varname, description))
+        elif is_common:
+            common_vars.append((varname, description))
+
+    # Remove duplicates within each category
+    common_vars = list(set(common_vars))
+    for arch in arch_vars:
+        arch_vars[arch] = list(set(arch_vars[arch]))
+    for board in board_vars:
+        board_vars[board] = list(set(board_vars[board]))
+
+    # Generate ReST documentation
+    rst = generate_rst(common_vars, arch_vars, board_vars)
+
+    # Write to output file
+
+    # Only write the new rst if it differs from the old one.
+    hash_old = hashlib.sha1()
+    try:
+        f = open(output_file, 'rb')
+        hash_old.update(f.read())
+    except Exception:
+        pass
+
+    hash_new = hashlib.sha1()
+    hash_new.update(rst.encode('utf-8'))
+    if hash_old.hexdigest() == hash_new.hexdigest():
+        return
+
+    with open(output_file, 'w') as f:
+        f.write(rst)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/Documentation/user/variables.rst b/Documentation/user/variables.rst
index cb1296c06d02..61808f1d72de 100644
--- a/Documentation/user/variables.rst
+++ b/Documentation/user/variables.rst
@@ -94,13 +94,13 @@ This example changes the partitioning of the nand0 device:
 .. _magicvars:
 
 Magic variables
----------------
+===============
 
 Some variables have special meanings and influence the behaviour
 of barebox. Most but not all of them are consolidated in the :ref:`global_device`.
 Since it's hard to remember which variables these are and if the current
 barebox has support for them the :ref:`command_magicvar` command can print a list
-of all variables with special meaning along with a short description:
+of all active variables with special meaning along with a short description:
 
 .. code-block:: console
 
@@ -108,9 +108,6 @@ of all variables with special meaning along with a short description:
   OPTARG                           optarg for hush builtin getopt
   PATH                             colon separated list of paths to search for executables
   PS1                              hush prompt
-  armlinux_architecture            ARM machine ID
-  armlinux_system_rev              ARM system revision
-  armlinux_system_serial           ARM system serial
   automount_path                   mountpath passed to automount scripts
   bootargs                         Linux Kernel parameters
   bootsource                       The source barebox has been booted from
@@ -118,3 +115,7 @@ of all variables with special meaning along with a short description:
   global.boot.default              default boot order
   ...
 
+Above output will only list magic variables that were compiled.
+Below is a full listing of all currently documented magic variables:
+
+.. include:: magicvars.rst
-- 
2.47.3




  parent reply	other threads:[~2025-12-15  7:59 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-12-15  7:58 [PATCH 1/5] net: 9p: fix variable name in BAREBOX_MAGICVAR Ahmad Fatoum
2025-12-15  7:58 ` [PATCH 2/5] bootchooser: inline document global.bootchooser.reset_priorities Ahmad Fatoum
2025-12-15  7:58 ` [PATCH 3/5] reset_source: inline document global.system.reset* Ahmad Fatoum
2025-12-15  7:59 ` Ahmad Fatoum [this message]
2025-12-15  7:59 ` [PATCH 5/5] Documentation: turn magic variables literals into references Ahmad Fatoum
2025-12-15 11:17 ` [PATCH 1/5] net: 9p: fix variable name in BAREBOX_MAGICVAR Sascha Hauer
2025-12-15 12:17   ` Ahmad Fatoum
2025-12-15 12:23     ` Sascha Hauer
2025-12-15 12:25       ` Ahmad Fatoum

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251215075907.1222551-4-a.fatoum@pengutronix.de \
    --to=a.fatoum@pengutronix.de \
    --cc=barebox@lists.infradead.org \
    --cc=claude@anthropic.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox