gen_config.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. gen_config.py
  5. ┌----------------------------------------------------------------------------┐
  6. │ Nuclei SDK CLI test-configuration generator │
  7. │ │
  8. │ Automatically produces a JSON test matrix from a list of ARCH/ABI strings │
  9. │ with the following built-in rules: │
  10. │ 1. Extensions are randomly appended (_zba, _zbb …). Extensions that │
  11. │ end with "_x" (_xxlcz, _xxldsp) are always placed last. │
  12. │ 2. 32-/64-bit CORE is selected automatically (rv64* is only paired │
  13. │ with a core whose name contains the letter "x"). │
  14. │ 3. DOWNLOAD type "ddr" is restricted to the 600/900-series cores. │
  15. └----------------------------------------------------------------------------┘
  16. Usage examples
  17. --------------
  18. # Use the built-in list, add 2 random extensions, pretty-print
  19. $ python gen_config.py -e 2 -p
  20. # Supply your own list and write the result to disk
  21. $ python gen_config.py -i my.list -o config.json -p
  22. # Pipe the list through stdin
  23. $ cat my.list | python gen_config.py -e 1 -p
  24. # Full configuration with custom template
  25. $ python gen_config.py -t custom.json -f -o full.json -e 3 -p
  26. """
  27. import json
  28. import random
  29. import argparse
  30. import sys
  31. import re
  32. from typing import List, Dict, Tuple
  33. # --------------------------------------------------------------------------- #
  34. # Constants #
  35. # --------------------------------------------------------------------------- #
  36. # Valid download memory types for the build configuration.
  37. DOWNLOAD_OPTS = ["ilm", "flash", "flashxip", "sram", "ddr"]
  38. # Extension pools for random selection.
  39. # Note: Extensions ending in "_x" must appear at the end of the architecture string.
  40. X_EXT = ["_xxlcz", "_xxldsp"] # Special extensions that must be last.
  41. Z_EXT = ["_zba", "_zbb", "_zbc", "_zbs", "_zicond", "_zk", "_zks"] # Standard Z extensions.
  42. # --------------------------------------------------------------------------- #
  43. # Default Configuration Template
  44. # This serves as the base structure when --full is used without a custom template.
  45. # --------------------------------------------------------------------------- #
  46. DEFAULT_TEMPLATE = {
  47. "run_config": {
  48. "target": "qemu",
  49. "hardware": {"baudrate": 115200, "timeout": 240},
  50. "qemu": {"timeout": 240},
  51. },
  52. "parallel": "-j",
  53. "build_target": "clean all",
  54. "build_config": {"SOC": "evalsoc", "BOARD": "nuclei_fpga_eval", "ARCH_EXT": ""},
  55. "build_configs": {}, # Will be populated by generated configurations.
  56. "appconfig": {},
  57. "expected": {
  58. "application/baremetal/demo_nice": {"run": True, "build": True},
  59. "application/baremetal/demo_dsp": {"run": False, "build": False}
  60. }
  61. }
  62. # Default list of ARCH/ABI pairs to use if no input is provided.
  63. DEFAULT_ARCH_ABI = [
  64. "rv32emc_zfinx/ilp32e",
  65. "rv32emc_zdinx/ilp32e",
  66. "rv32emac_zfinx/ilp32e",
  67. "rv32emac_zdinx/ilp32e",
  68. "rv32imc_zfinx/ilp32",
  69. "rv32imc_zdinx/ilp32",
  70. "rv32em_zfinx_zca_zcb_zcmp/ilp32e",
  71. "rv32em_zdinx_zca_zcb_zcmp/ilp32e",
  72. "rv32ema_zfinx_zca_zcb_zcmp/ilp32e",
  73. "rv32ema_zdinx_zca_zcb_zcmp/ilp32e",
  74. "rv32im_zfinx_zca_zcb_zcmp/ilp32",
  75. "rv32im_zdinx_zca_zcb_zcmp/ilp32",
  76. "rv32imac_zfinx/ilp32",
  77. "rv32imac_zdinx/ilp32",
  78. "rv32ima_zfinx_zca_zcb_zcmp/ilp32",
  79. "rv32ima_zdinx_zca_zcb_zcmp/ilp32",
  80. "rv64imac_zfinx/lp64",
  81. "rv64imac_zdinx/lp64",
  82. ]
  83. # Mapping from base architecture strings to corresponding Nuclei CPU core names.
  84. # Used to select an appropriate CORE based on the RISC-V ISA string.
  85. CORE_ARCH_MAP = {
  86. "rv32imc": "n200",
  87. "rv32emc": "n200e",
  88. "rv32iac": "n201",
  89. "rv32eac": "n201e",
  90. "rv32ic": "n202",
  91. "rv32ec": "n202e",
  92. "rv32emac": "n203e",
  93. "rv32imac": "n300",
  94. "rv32imafc": "n300f",
  95. "rv32imafdc": "n300fd",
  96. "rv64imac": "nx900",
  97. "rv64imafc": "nx900f",
  98. "rv64imafdc": "nx900fd",
  99. }
  100. # --------------------------------------------------------------------------- #
  101. # Helper functions #
  102. # --------------------------------------------------------------------------- #
  103. def pick_extensions(max_cnt: int) -> str:
  104. """
  105. Randomly select up to `max_cnt` extensions from Z_EXT and X_EXT.
  106. Rules:
  107. - Total number of selected extensions <= max_cnt.
  108. - Z extensions are chosen first (random subset, then sorted).
  109. - X extensions (ending in '_x') are chosen from remaining count and appended last.
  110. - Result is a concatenated string (e.g., '_zba_zbb_xxldsp').
  111. """
  112. if max_cnt <= 0:
  113. return ""
  114. # Randomly decide how many Z extensions to pick (0 to min(len(Z_EXT), max_cnt))
  115. n_z = random.randint(0, min(len(Z_EXT), max_cnt))
  116. picked_z = sorted(random.sample(Z_EXT, n_z)) # Sort for deterministic ordering
  117. # Remaining slots for X extensions
  118. remain = max_cnt - n_z
  119. n_x = random.randint(0, min(len(X_EXT), remain))
  120. picked_x = sorted(random.sample(X_EXT, n_x)) # Also sorted
  121. # Z extensions come first, X extensions last (as required)
  122. return "".join(picked_z + picked_x)
  123. def pick_core_archext(arch: str) -> Tuple[str, str]:
  124. """
  125. Given a full RISC-V architecture string (e.g., 'rv32imacbv_zfinx'),
  126. this function:
  127. 1. Separates base architecture from standard extensions (after '_').
  128. 2. Handles special suffixes like 'b', 'v', 'bv', 'vb' that are part of the base ISA.
  129. 3. Maps the cleaned base architecture to a Nuclei CORE using CORE_ARCH_MAP.
  130. 4. Returns the matched CORE name and the formatted ARCH_EXT string.
  131. Example:
  132. Input: "rv32imacbv_zfinx"
  133. Output: ("n300", "bv_zfinx")
  134. The ARCH_EXT field in the output config will be "_bv_zfinx" (with leading underscore).
  135. """
  136. # Split into base (before first '_') and extension part (after first '_')
  137. arch_base, _, archext = arch.partition('_')
  138. # Check if base ends with special suffixes: 'b', 'v', 'bv', or 'vb'
  139. match = re.match(r'^(.*?)(bv|vb|b|v)$', arch_base)
  140. if match:
  141. # Extract the true base (without suffix) and the suffix
  142. arch_new, suffix = match.groups()
  143. # Combine suffix with original extension (if any)
  144. combined_ext = f"{suffix}_{archext}" if archext else suffix
  145. final_archext = f"_{combined_ext}" # Always include leading underscore for non-empty
  146. else:
  147. # No special suffix; base is unchanged
  148. arch_new = arch_base
  149. combined_ext = archext
  150. final_archext = f"_{combined_ext}" if combined_ext else ""
  151. # Find the best matching core in CORE_ARCH_MAP
  152. best_match_core = None
  153. best_match_len = float('inf') # Prefer shortest matching prefix (more specific)
  154. for map_arch, core_name in CORE_ARCH_MAP.items():
  155. # Only consider map entries that start with our cleaned base
  156. if map_arch.startswith(arch_new):
  157. # Among matches, pick the one with the shortest key (more precise match)
  158. if len(map_arch) < best_match_len:
  159. best_match_core = core_name
  160. best_match_len = len(map_arch)
  161. if best_match_core is None:
  162. raise ValueError(f"Could not find a matching core for arch: {arch_new}")
  163. return best_match_core, final_archext
  164. def pick_download(core: str) -> str:
  165. """
  166. Select a random DOWNLOAD type from DOWNLOAD_OPTS.
  167. Constraint:
  168. - 'ddr' is only allowed for cores containing '600' or '900' in their name
  169. (e.g., 'nx900', 'n600' — though only 900 appears in current map).
  170. """
  171. pool = DOWNLOAD_OPTS.copy()
  172. # Remove 'ddr' if core is not in 600/900 series
  173. if "600" not in core and "900" not in core:
  174. pool.remove("ddr")
  175. return random.choice(pool)
  176. def optimize_archext(archext: str) -> str:
  177. """
  178. Optimize archext string for compatibility with Nuclei Qemu.
  179. - Replace '_zdinx' with '_zfinx_zdinx' if present for Nuclei Qemu 2025.02.
  180. """
  181. if "_zdinx" in archext and "_zfinx" not in archext:
  182. archext = archext.replace("_zdinx", "_zfinx_zdinx")
  183. return archext
  184. def build_one(arch_abi: str, max_ext: int) -> Dict[str, Dict[str, str]]:
  185. """
  186. Process a single "ARCH/ABI" string (e.g., "rv32imc_zfinx/ilp32") into a full config entry.
  187. Steps:
  188. 1. Split into ARCH and ABI.
  189. 2. Append up to `max_ext` random extensions to ARCH.
  190. 3. Determine CORE and ARCH_EXT using `pick_core_archext`.
  191. 4. Choose DOWNLOAD type based on CORE.
  192. 5. Return a dict with a unique key: "{final_arch}-{download}".
  193. """
  194. if "/" not in arch_abi:
  195. raise ValueError(f"Malformed arch_abi: {arch_abi}")
  196. arch, abi = arch_abi.strip().split("/", 1)
  197. # Append randomly selected extensions to the base architecture string
  198. final_arch = arch + pick_extensions(max_ext)
  199. # Determine core and formatted extension string
  200. core, archext = pick_core_archext(final_arch)
  201. download = pick_download(core)
  202. archext = optimize_archext(archext)
  203. # Create a unique key for this configuration
  204. key = f"{final_arch}-{download}"
  205. return {
  206. key: {
  207. "DOWNLOAD": download,
  208. "CORE": core,
  209. "RISCV_ARCH": final_arch,
  210. "RISCV_ABI": abi,
  211. "ARCH_EXT": archext,
  212. }
  213. }
  214. def build_all(lines: List[str], max_ext: int) -> Dict:
  215. """
  216. Process a list of ARCH/ABI strings (one per line), skipping empty lines and comments.
  217. Returns a dictionary of all generated configurations.
  218. """
  219. result = {}
  220. for line in lines:
  221. line = line.strip()
  222. if not line or line.startswith("#"):
  223. continue # Skip blank lines and comments
  224. result.update(build_one(line, max_ext))
  225. return result
  226. # --------------------------------------------------------------------------- #
  227. # Command-line interface #
  228. # --------------------------------------------------------------------------- #
  229. def main(argv=None):
  230. """
  231. Main entry point. Parses arguments, reads input, generates config, and outputs JSON.
  232. """
  233. parser = argparse.ArgumentParser(
  234. description="Nuclei SDK CLI test-configuration generator — "
  235. "randomly pick CORE/DOWNLOAD/extension suffixes while "
  236. "respecting built-in constraints.",
  237. epilog=(
  238. "Examples:\n"
  239. " python gen_config.py -e 2 -p\n"
  240. " # Use built-in ARCH list, append 2 random extensions, pretty-print to stdout\n\n"
  241. " python gen_config.py -i my.list -o config.json -p\n"
  242. " # Read ARCH/ABI pairs from my.list, write pretty JSON to config.json\n\n"
  243. " cat my.list | python gen_config.py -e 1 -p\n"
  244. " # Read ARCH/ABI from stdin, append 1 random extension, pretty-print\n\n"
  245. " python gen_config.py -t custom.json -f -o full.json -e 3\n"
  246. " # Load custom template, generate full config with 3 random extensions\n\n"
  247. "Built-in rules:\n"
  248. " * RV64* architectures are only paired with CORE names containing 'x'.\n"
  249. " * RV32* architectures must use CORE names without 'x'.\n"
  250. " * DOWNLOAD 'ddr' is restricted to 600/900-series cores.\n"
  251. " * Extensions ending with '_x' are always placed last.\n"
  252. ),
  253. formatter_class=argparse.RawTextHelpFormatter,
  254. )
  255. parser.add_argument(
  256. "-i", "--input",
  257. type=str,
  258. help="Text file containing ARCH/ABI pairs (one per line). "
  259. "If omitted and stdin is empty, the built-in list is used.",
  260. )
  261. parser.add_argument(
  262. "-o", "--output",
  263. type=str,
  264. help="Output JSON file. If omitted, the result is printed to stdout.",
  265. )
  266. parser.add_argument(
  267. "-e", "--ext",
  268. type=int,
  269. default=0,
  270. help="Maximum number of random extensions to append (default: 0).",
  271. )
  272. parser.add_argument(
  273. "-p", "--pretty",
  274. action="store_true",
  275. help="Pretty-print the resulting JSON (indent 2).",
  276. )
  277. parser.add_argument(
  278. "-q", "--quiet",
  279. action="store_true",
  280. help="Suppress runtime messages.",
  281. )
  282. parser.add_argument(
  283. "-t", "--template",
  284. metavar="FILE",
  285. help="Path to custom JSON template. If omitted, a built-in template is used.",
  286. )
  287. parser.add_argument(
  288. "-f", "--full",
  289. action="store_true",
  290. help="Emit the full configuration object (template + build_configs) "
  291. "instead of only the build_configs section.",
  292. )
  293. args = parser.parse_args(argv)
  294. # Load configuration template (custom or default)
  295. if args.template:
  296. with open(args.template, encoding="utf-8") as f:
  297. template = json.load(f)
  298. else:
  299. template = DEFAULT_TEMPLATE
  300. # Read input ARCH/ABI list
  301. if args.input:
  302. # Read from specified file
  303. with open(args.input, encoding="utf-8") as f:
  304. lines = f.readlines()
  305. else:
  306. # If stdin has data (e.g., piped input), use it; otherwise use built-in list
  307. lines = sys.stdin.readlines() if not sys.stdin.isatty() else DEFAULT_ARCH_ABI
  308. if not args.quiet:
  309. print(f"/* Generating configuration for {len(lines)} ARCH/ABI entries ... */")
  310. # Generate all build configurations
  311. build_configs = build_all(lines, args.ext)
  312. # Decide what to output: full template or just build_configs
  313. if args.full:
  314. template["build_configs"] = build_configs
  315. data = template
  316. else:
  317. data = build_configs
  318. # Serialize to JSON
  319. out = json.dumps(data, indent=2) if args.pretty else json.dumps(data)
  320. # Write to file or stdout
  321. if args.output:
  322. with open(args.output, "w", encoding="utf-8") as f:
  323. f.write(out)
  324. print(f"/* Written to {args.output} */")
  325. else:
  326. print(out)
  327. if __name__ == "__main__":
  328. main()