1#!/usr/bin/env python3
2
3# Copyright (c) 2025 Arduino SA
4# SPDX-License-Identifier: Apache-2.0
5
6import argparse
7import os
8import pickle
9
10from tabulate import tabulate
11
12KIND_TO_COL = {
13    "unset": "not set",
14    "default": "default",
15    "assign": "assigned",
16    "select": "selected",
17    "imply": "implied",
18}
19
20
21def source_link(refpath, fn, ln):
22    if refpath:
23        fn = os.path.relpath(fn, refpath)
24
25    link_fn = fn
26    disp_fn = os.path.normpath(fn).replace("../", "").replace("_", "\\_")
27
28    return f"[{disp_fn}:{ln}](<{link_fn}#L{ln}>)"
29
30
31def write_markdown(trace_data, output):
32    sections = {"user": [], "hidden": [], "unset": []}
33
34    if os.name == "nt":
35        # relative paths on Windows can't span drives, so don't use them
36        # generated links will be absolute
37        refpath = None
38    else:
39        refpath = os.path.dirname(os.path.abspath(output))
40
41    for sym in trace_data:
42        sym_name, sym_vis, sym_type, sym_value, sym_src, sym_loc = sym
43        if sym_vis == "n":
44            section = "hidden"
45        elif sym_src == "unset":
46            section = "unset"
47        else:
48            section = "user"
49
50        if section == "user":
51            sym_name = f"`{sym_name}`"
52        elif section == "hidden":
53            sym_name = f"`{sym_name}` (h)"
54
55        if sym_type == "string" and sym_value is not None:
56            sym_value = f'"{sym_value}"'.replace("_", "\\_")
57
58        if isinstance(sym_loc, tuple):
59            sym_loc = source_link(refpath, *sym_loc)
60        elif isinstance(sym_loc, list):
61            sym_loc = " || <br> ".join(f"`{loc}`" for loc in sym_loc)
62            sym_loc = sym_loc.replace("|", "\\|")
63        elif sym_loc is None and sym_src == "default":
64            sym_loc = "_(implicit)_"
65
66        sym_src = KIND_TO_COL[sym_src]
67
68        sections[section].append((sym_type, sym_name, sym_value, sym_src, sym_loc))
69
70    lines = []
71    add = lines.append
72
73    headers = ["Type", "Name", "Value", "Source", "Location"]
74    colaligns = ["right", "left", "right", "center", "left"]
75
76    add("\n## Visible symbols\n\n")
77    add(
78        tabulate(
79            sorted(sections["user"], key=lambda x: x[1]),
80            headers=headers,
81            tablefmt='pipe',
82            colalign=colaligns,
83        )
84    )
85
86    add("\n\n## Invisible symbols\n\n")
87    add(
88        tabulate(
89            sorted(sections["hidden"], key=lambda x: x[1]),
90            headers=headers,
91            tablefmt='pipe',
92            colalign=colaligns,
93        )
94    )
95
96    add("\n\n## Unset symbols\n\n")
97    for sym_name in sorted(x[1] for x in sections["unset"]):
98        add(f"    # {sym_name} is not set\n")
99
100    with open(output, "w") as f:
101        f.writelines(lines)
102
103
104def main():
105    parser = argparse.ArgumentParser(allow_abbrev=False)
106    parser.add_argument("dotconfig_file", help="Input merged .config file")
107    parser.add_argument("output_file", help="Output Markdown file")
108    parser.add_argument("kconfig_file", help="Top-level Kconfig file", nargs="?")
109
110    args = parser.parse_args()
111
112    with open(args.dotconfig_file + '-trace.pickle', 'rb') as f:
113        trace_data = pickle.load(f)
114    write_markdown(trace_data, args.output_file)
115
116
117if __name__ == '__main__':
118    main()
119