1"""
2Utility script to migrate Zephyr-based projects to normative POSIX Kconfig options.
3
4This script should be used for migrating from versions of Zephyr older than v3.7.0 to Zephyr
5version v3.7.0 or later.
6
7Usage::
8
9    python $ZEPHYR_BASE/scripts/utils/migrate_posix_kconfigs.py --root root_path --dry-run
10
11The utility will process c, cpp, h, hpp, rst, conf, CMakeLists.txt,
12yml, yaml and Kconfig files.
13
14
15Copyright (c) 2022 Nordic Semiconductor ASA
16Copyright (c) 2024 Tenstorrent AI ULC
17SPDX-License-Identifier: Apache-2.0
18"""
19
20import argparse
21import re
22import sys
23from pathlib import Path
24
25ZEPHYR_BASE = Path(__file__).parents[2]
26
27FILE_PATTERNS = (
28    r".+\.c", r".+\.cpp", r".+\.hpp", r".+\.h", r".+\.rst", r".+\.conf",
29    r".+\.yml", r".+\.yaml", r"CMakeLists.txt", r"Kconfig(\..+)?"
30)
31
32REPLACEMENTS = {
33    "EVENTFD_MAX": "ZVFS_EVENTFD_MAX",
34    "FNMATCH": "POSIX_C_LIB_EXT",
35    "GETENTROPY": "POSIX_C_LIB_EXT",
36    "GETOPT": "POSIX_C_LIB_EXT",
37    "MAX_PTHREAD_COUNT": "POSIX_THREAD_THREADS_MAX",
38    "MAX_PTHREAD_KEY_COUNT": "POSIX_THREAD_KEYS_MAX",
39    "MAX_TIMER_COUNT": "POSIX_TIMER_MAX",
40    "MSG_COUNT_MAX": "POSIX_MQ_OPEN_MAX",
41    "POSIX_CLOCK": "POSIX_TIMERS",
42    "POSIX_CONFSTR": "POSIX_SINGLE_PROCESS",
43    "POSIX_ENV": "POSIX_SINGLE_PROCESS",
44    "POSIX_FS": "POSIX_FILE_SYSTEM",
45    "POSIX_LIMITS_RTSIG_MAX": "POSIX_RTSIG_MAX",
46    "POSIX_MAX_FDS": "ZVFS_OPEN_MAX",
47    "POSIX_MAX_OPEN_FILES": "ZVFS_OPEN_MAX",
48    "POSIX_MQUEUE": "POSIX_MESSAGE_PASSING",
49    "POSIX_PUTMSG": "XOPEN_STREAMS",
50    "POSIX_SIGNAL": "POSIX_SIGNALS",
51    "POSIX_SYSCONF": "POSIX_SINGLE_PROCESS",
52    "POSIX_SYSLOG": "XSI_SYSTEM_LOGGING",
53    "POSIX_UNAME": "POSIX_SINGLE_PROCESS",
54    "PTHREAD": "POSIX_THREADS",
55    "PTHREAD_BARRIER": "POSIX_BARRIERS",
56    "PTHREAD_COND": "POSIX_THREADS",
57    "PTHREAD_IPC": "POSIX_THREADS",
58    "PTHREAD_KEY": "POSIX_THREADS",
59    "PTHREAD_MUTEX": "POSIX_THREADS",
60    "PTHREAD_RWLOCK": "POSIX_READER_WRITER_LOCKS",
61    "PTHREAD_SPINLOCK": "POSIX_SPIN_LOCKS",
62    "TIMER": "POSIX_TIMERS",
63    "TIMER_DELAYTIMER_MAX": "POSIX_DELAYTIMER_MAX",
64    "SEM_NAMELEN_MAX": "POSIX_SEM_NAME_MAX",
65    "SEM_VALUE_MAX": "POSIX_SEM_VALUE_MAX",
66}
67
68MESSAGES = {
69    "POSIX_CLOCK":
70        "POSIX_CLOCK is a one-to-many replacement. If this simple substitution is not "
71        "sufficient, it's best to try a combination of POSIX_CLOCK_SELECTION, POSIX_CPUTIME, "
72        "POSIX_MONOTONIC_CLOCK, POSIX_TIMERS, and POSIX_TIMEOUTS.",
73    "POSIX_MAX_FDS":
74        "A read-only version of this symbol is POSIX_OPEN_MAX, which is of course, the standard "
75        "symbol. ZVFS_OPEN_MAX may be set by the user. Consider using POSIX_MAX_FDS if the "
76        "use-case is read-only.",
77}
78
79
80def process_file(path):
81    modified = False
82    output = []
83
84    try:
85        with open(path) as f:
86            lines = f.readlines()
87
88            lineno = 1
89            for line in lines:
90                longest = ""
91                length = 0
92                for m in REPLACEMENTS:
93                    if re.match(".*" + m + ".*", line) and len(m) > length:
94                        length = len(m)
95                        longest = m
96
97                # TIMER is just way too frequent an occurrence. Skip it.
98                skip = {"TIMER"}
99                if length != 0:
100                    if longest in skip:
101                        pass
102                    else:
103                        modified = True
104                        old = line
105                        line = line.replace(longest, REPLACEMENTS[longest])
106                        msg = MESSAGES.get(longest)
107                        if msg:
108                            print(f"{path}:{lineno}:old:{old.strip()}")
109                            print(f"{path}:{lineno}:new:{line.strip()}")
110
111                lineno += 1
112                output.append(line)
113
114        if modified is False or args.dry_run:
115            return
116
117        with open(path, "w") as f:
118            f.writelines(output)
119
120    except UnicodeDecodeError:
121        print(f"Unable to read lines from {path}", file=sys.stderr)
122    except Exception as e:
123        print(f"Failed with exception {e}", e)
124
125
126def process_tree(project):
127    for p in project.glob("**/*"):
128        for fp in FILE_PATTERNS:
129            cfp = re.compile(".+/" + fp + "$")
130            if re.match(cfp, str(p)) is not None:
131                process_file(p)
132
133
134if __name__ == "__main__":
135    parser = argparse.ArgumentParser(allow_abbrev=False)
136    parser.add_argument(
137        "-r",
138        "--root",
139        type=Path,
140        required=True,
141        help="Zephyr-based project path")
142    parser.add_argument(
143        "-d",
144        "--dry-run",
145        action="store_true",
146        help="log potential changes without modifying files")
147    args = parser.parse_args()
148
149    process_tree(args.root)
150