Skip to content

Commit dbb398a

Browse files
committed
tools: add nuclei sdk cli test configuration generator script gen_config.py
The nuclei_fpga_eval_ci_*zxinx*.json build_configs key's data is generated by this script, this json file is used to test with or without zfinx/zdinx extension and newlib/libncrt library test matrix Currently the ci jobs are not yet added, still internal tested in !91 eg. python3 tools/scripts/misc/gen_config.py -p -i tools/scripts/nsdk_cli/configs/gencfgs/gencfg_wo_zxinx.txt Signed-off-by: Huaqi Fang <578567190@qq.com>
1 parent cd2b85e commit dbb398a

File tree

8 files changed

+942
-0
lines changed

8 files changed

+942
-0
lines changed

doc/source/changelog.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ This is release version of ``0.9.0`` of Nuclei SDK, which is still under develop
5757

5858
- Add ``demo_eclic_umode`` nsdk_cli run configuration for daily ci running
5959
- Support ``matrix`` field in ``appcfg`` or ``hwcfg`` for ``nsdk_bench.py``
60+
- Add a new utility script, ``gen_config.py``, to automate the generation of ``build_configs`` sections in nsdk_cli config file.
61+
This tool accelerates the development of test suites by producing diverse configuration sets. As an initial implementation,
62+
the ``build_configs`` section in ``nuclei_fpga_eval_ci_*zxinx*.json`` have been migrated to be generated by this script.
6063

6164
V0.8.1
6265
------

tools/scripts/misc/gen_config.py

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

0 commit comments

Comments
 (0)