1#!/usr/bin/env python
2"""
3Utility script to assist in the migration of a board from hardware model v1
4(HWMv1) to hardware model v2 (HWMv2).
5
6.. warning::
7    This script is not a complete migration tool. It is meant to assist in the
8    migration process, but it does not handle all cases.
9
10This script requires the following arguments:
11
12- ``-b|--board``: The name of the board to migrate.
13- ``-g|--group``: The group the board belongs to. This is used to group a set of
14  boards in the same folder. In HWMv2, the boards are no longer organized by
15  architecture.
16- ``-v|--vendor``: The vendor name.
17- ``-s|--soc``: The SoC name.
18
19In some cases, the new board name will differ from the old board name. For
20example, the old board name may have the SoC name as a suffix, while in HWMv2,
21this is no longer needed. In such cases, ``-n|--new-board`` needs to be
22provided.
23
24For boards with variants, ``--variants`` needs to be provided.
25
26For out-of-tree boards, provide ``--board-root`` pointing to the custom board
27root.
28
29Copyright (c) 2023 Nordic Semiconductor ASA
30SPDX-License-Identifier: Apache-2.0
31"""
32
33import argparse
34from pathlib import Path
35import re
36import sys
37
38import ruamel.yaml
39
40
41ZEPHYR_BASE = Path(__file__).parents[2]
42
43
44def board_v1_to_v2(board_root, board, new_board, group, vendor, soc, variants):
45    try:
46        board_path = next(board_root.glob(f"boards/*/{board}"))
47    except StopIteration:
48        sys.exit(f"Board not found: {board}")
49
50    new_board_path = board_root / "boards" / group / new_board
51    if new_board_path.exists():
52        print("New board already exists, updating board with additional SoC")
53        if not soc:
54            sys.exit("No SoC provided")
55
56    new_board_path.mkdir(parents=True, exist_ok=True)
57
58    print("Moving files to the new board folder...")
59    for f in board_path.iterdir():
60        f_new = new_board_path / f.name
61        if f_new.exists():
62            print(f"Skipping existing file: {f_new}")
63            continue
64        f.rename(f_new)
65
66    print("Creating or updating board.yaml...")
67    board_settings_file = new_board_path / "board.yml"
68    if not board_settings_file.exists():
69        board_settings = {
70            "board": {
71                "name": new_board,
72                "vendor": vendor,
73                "socs": []
74            }
75        }
76    else:
77        with open(board_settings_file) as f:
78            yaml = ruamel.yaml.YAML(typ='safe', pure=True)
79            board_settings = yaml.load(f) # pylint: disable=assignment-from-no-return
80
81    soc = {"name": soc}
82    if variants:
83        soc["variants"] = [{"name": variant} for variant in variants]
84
85    board_settings["board"]["socs"].append(soc)
86
87    yaml = ruamel.yaml.YAML()
88    yaml.indent(sequence=4, offset=2)
89    with open(board_settings_file, "w") as f:
90        yaml.dump(board_settings, f)
91
92    print(f"Updating {board}_defconfig...")
93    board_defconfig_file = new_board_path / f"{board}_defconfig"
94    with open(board_defconfig_file) as f:
95        board_soc_settings = []
96        board_defconfig = ""
97
98        dropped_line = False
99        for line in f.readlines():
100            m = re.match(r"^CONFIG_BOARD_.*$", line)
101            if m:
102                dropped_line = True
103                continue
104
105            m = re.match(r"^CONFIG_(SOC_[A-Z0-9_]+).*$", line)
106            if m:
107                dropped_line = True
108                if not re.match(r"^CONFIG_SOC_SERIES_.*$", line):
109                    board_soc_settings.append(m.group(1))
110                continue
111
112            if dropped_line and re.match(r"^$", line):
113                continue
114
115            dropped_line = False
116            board_defconfig += line
117
118    with open(board_defconfig_file, "w") as f:
119        f.write(board_defconfig)
120
121    print("Updating Kconfig.defconfig...")
122    board_kconfig_defconfig_file = new_board_path / "Kconfig.defconfig"
123    with open(board_kconfig_defconfig_file) as f:
124        board_kconfig_defconfig = ""
125        has_kconfig_defconfig_entries = False
126
127        in_board = False
128        for line in f.readlines():
129            # drop "config BOARD" entry from Kconfig.defconfig
130            m = re.match(r"^config BOARD$", line)
131            if m:
132                in_board = True
133                continue
134
135            if in_board and re.match(r"^\s+.*$", line):
136                continue
137
138            in_board = False
139
140            m = re.match(r"^config .*$", line)
141            if m:
142                has_kconfig_defconfig_entries = True
143
144            m = re.match(rf"^(.*)BOARD_{board.upper()}(.*)$", line)
145            if m:
146                board_kconfig_defconfig += (
147                    m.group(1) + "BOARD_" + new_board.upper() + m.group(2) + "\n"
148                )
149                continue
150
151            board_kconfig_defconfig += line
152
153    if has_kconfig_defconfig_entries:
154        with open(board_kconfig_defconfig_file, "w") as f:
155            f.write(board_kconfig_defconfig)
156    else:
157        print("Removing empty Kconfig.defconfig after update...")
158        board_kconfig_defconfig_file.unlink()
159
160    print(f"Creating or updating Kconfig.{new_board}...")
161    board_kconfig_file = new_board_path / "Kconfig.board"
162    copyright = None
163    with open(board_kconfig_file) as f:
164        for line in f.readlines():
165            if "Copyright" in line:
166                copyright = line
167    new_board_kconfig_file = new_board_path / f"Kconfig.{new_board}"
168    header = "# SPDX-License-Identifier: Apache-2.0\n"
169    if copyright is not None:
170        header = copyright + header
171    selects = "\n\t" + "\n\t".join(["select " + setting for setting in board_soc_settings]) + "\n"
172    if not new_board_kconfig_file.exists():
173        with open(new_board_kconfig_file, "w") as f:
174            f.write(
175                header +
176                f"\nconfig BOARD_{new_board.upper()}{selects}"
177            )
178    else:
179        with open(new_board_kconfig_file, "a") as f:
180            f.write(selects)
181
182    print("Removing old Kconfig.board...")
183    board_kconfig_file.unlink()
184
185    print("Conversion done!")
186
187
188if __name__ == "__main__":
189    parser = argparse.ArgumentParser(allow_abbrev=False)
190
191    parser.add_argument(
192        "--board-root",
193        type=Path,
194        default=ZEPHYR_BASE,
195        help="Board root",
196    )
197
198    parser.add_argument("-b", "--board", type=str, required=True, help="Board name")
199    parser.add_argument("-n", "--new-board", type=str, help="New board name")
200    parser.add_argument("-g", "--group", type=str, required=True, help="Board group")
201    parser.add_argument("-v", "--vendor", type=str, required=True, help="Vendor name")
202    parser.add_argument("-s", "--soc", type=str, required=True, help="Board SoC")
203    parser.add_argument("--variants", nargs="+", default=[], help="Board variants")
204
205    args = parser.parse_args()
206
207    board_v1_to_v2(
208        args.board_root,
209        args.board,
210        args.new_board or args.board,
211        args.group,
212        args.vendor,
213        args.soc,
214        args.variants,
215    )
216