| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- gen_config.py
- ┌----------------------------------------------------------------------------┐
- │ Nuclei SDK CLI test-configuration generator │
- │ │
- │ Automatically produces a JSON test matrix from a list of ARCH/ABI strings │
- │ with the following built-in rules: │
- │ 1. Extensions are randomly appended (_zba, _zbb …). Extensions that │
- │ end with "_x" (_xxlcz, _xxldsp) are always placed last. │
- │ 2. 32-/64-bit CORE is selected automatically (rv64* is only paired │
- │ with a core whose name contains the letter "x"). │
- │ 3. DOWNLOAD type "ddr" is restricted to the 600/900-series cores. │
- └----------------------------------------------------------------------------┘
- Usage examples
- --------------
- # Use the built-in list, add 2 random extensions, pretty-print
- $ python gen_config.py -e 2 -p
- # Supply your own list and write the result to disk
- $ python gen_config.py -i my.list -o config.json -p
- # Pipe the list through stdin
- $ cat my.list | python gen_config.py -e 1 -p
- # Full configuration with custom template
- $ python gen_config.py -t custom.json -f -o full.json -e 3 -p
- """
- import json
- import random
- import argparse
- import sys
- import re
- from typing import List, Dict, Tuple
- # --------------------------------------------------------------------------- #
- # Constants #
- # --------------------------------------------------------------------------- #
- # Valid download memory types for the build configuration.
- DOWNLOAD_OPTS = ["ilm", "flash", "flashxip", "sram", "ddr"]
- # Extension pools for random selection.
- # Note: Extensions ending in "_x" must appear at the end of the architecture string.
- X_EXT = ["_xxlcz", "_xxldsp"] # Special extensions that must be last.
- Z_EXT = ["_zba", "_zbb", "_zbc", "_zbs", "_zicond", "_zk", "_zks"] # Standard Z extensions.
- # --------------------------------------------------------------------------- #
- # Default Configuration Template
- # This serves as the base structure when --full is used without a custom template.
- # --------------------------------------------------------------------------- #
- DEFAULT_TEMPLATE = {
- "run_config": {
- "target": "qemu",
- "hardware": {"baudrate": 115200, "timeout": 240},
- "qemu": {"timeout": 240},
- },
- "parallel": "-j",
- "build_target": "clean all",
- "build_config": {"SOC": "evalsoc", "BOARD": "nuclei_fpga_eval", "ARCH_EXT": ""},
- "build_configs": {}, # Will be populated by generated configurations.
- "appconfig": {},
- "expected": {
- "application/baremetal/demo_nice": {"run": True, "build": True},
- "application/baremetal/demo_dsp": {"run": False, "build": False}
- }
- }
- # Default list of ARCH/ABI pairs to use if no input is provided.
- DEFAULT_ARCH_ABI = [
- "rv32emc_zfinx/ilp32e",
- "rv32emc_zdinx/ilp32e",
- "rv32emac_zfinx/ilp32e",
- "rv32emac_zdinx/ilp32e",
- "rv32imc_zfinx/ilp32",
- "rv32imc_zdinx/ilp32",
- "rv32em_zfinx_zca_zcb_zcmp/ilp32e",
- "rv32em_zdinx_zca_zcb_zcmp/ilp32e",
- "rv32ema_zfinx_zca_zcb_zcmp/ilp32e",
- "rv32ema_zdinx_zca_zcb_zcmp/ilp32e",
- "rv32im_zfinx_zca_zcb_zcmp/ilp32",
- "rv32im_zdinx_zca_zcb_zcmp/ilp32",
- "rv32imac_zfinx/ilp32",
- "rv32imac_zdinx/ilp32",
- "rv32ima_zfinx_zca_zcb_zcmp/ilp32",
- "rv32ima_zdinx_zca_zcb_zcmp/ilp32",
- "rv64imac_zfinx/lp64",
- "rv64imac_zdinx/lp64",
- ]
- # Mapping from base architecture strings to corresponding Nuclei CPU core names.
- # Used to select an appropriate CORE based on the RISC-V ISA string.
- CORE_ARCH_MAP = {
- "rv32imc": "n200",
- "rv32emc": "n200e",
- "rv32iac": "n201",
- "rv32eac": "n201e",
- "rv32ic": "n202",
- "rv32ec": "n202e",
- "rv32emac": "n203e",
- "rv32imac": "n300",
- "rv32imafc": "n300f",
- "rv32imafdc": "n300fd",
- "rv64imac": "nx900",
- "rv64imafc": "nx900f",
- "rv64imafdc": "nx900fd",
- }
- # --------------------------------------------------------------------------- #
- # Helper functions #
- # --------------------------------------------------------------------------- #
- def pick_extensions(max_cnt: int) -> str:
- """
- Randomly select up to `max_cnt` extensions from Z_EXT and X_EXT.
- Rules:
- - Total number of selected extensions <= max_cnt.
- - Z extensions are chosen first (random subset, then sorted).
- - X extensions (ending in '_x') are chosen from remaining count and appended last.
- - Result is a concatenated string (e.g., '_zba_zbb_xxldsp').
- """
- if max_cnt <= 0:
- return ""
- # Randomly decide how many Z extensions to pick (0 to min(len(Z_EXT), max_cnt))
- n_z = random.randint(0, min(len(Z_EXT), max_cnt))
- picked_z = sorted(random.sample(Z_EXT, n_z)) # Sort for deterministic ordering
- # Remaining slots for X extensions
- remain = max_cnt - n_z
- n_x = random.randint(0, min(len(X_EXT), remain))
- picked_x = sorted(random.sample(X_EXT, n_x)) # Also sorted
- # Z extensions come first, X extensions last (as required)
- return "".join(picked_z + picked_x)
- def pick_core_archext(arch: str) -> Tuple[str, str]:
- """
- Given a full RISC-V architecture string (e.g., 'rv32imacbv_zfinx'),
- this function:
- 1. Separates base architecture from standard extensions (after '_').
- 2. Handles special suffixes like 'b', 'v', 'bv', 'vb' that are part of the base ISA.
- 3. Maps the cleaned base architecture to a Nuclei CORE using CORE_ARCH_MAP.
- 4. Returns the matched CORE name and the formatted ARCH_EXT string.
- Example:
- Input: "rv32imacbv_zfinx"
- Output: ("n300", "bv_zfinx")
- The ARCH_EXT field in the output config will be "_bv_zfinx" (with leading underscore).
- """
- # Split into base (before first '_') and extension part (after first '_')
- arch_base, _, archext = arch.partition('_')
- # Check if base ends with special suffixes: 'b', 'v', 'bv', or 'vb'
- match = re.match(r'^(.*?)(bv|vb|b|v)$', arch_base)
- if match:
- # Extract the true base (without suffix) and the suffix
- arch_new, suffix = match.groups()
- # Combine suffix with original extension (if any)
- combined_ext = f"{suffix}_{archext}" if archext else suffix
- final_archext = f"_{combined_ext}" # Always include leading underscore for non-empty
- else:
- # No special suffix; base is unchanged
- arch_new = arch_base
- combined_ext = archext
- final_archext = f"_{combined_ext}" if combined_ext else ""
- # Find the best matching core in CORE_ARCH_MAP
- best_match_core = None
- best_match_len = float('inf') # Prefer shortest matching prefix (more specific)
- for map_arch, core_name in CORE_ARCH_MAP.items():
- # Only consider map entries that start with our cleaned base
- if map_arch.startswith(arch_new):
- # Among matches, pick the one with the shortest key (more precise match)
- if len(map_arch) < best_match_len:
- best_match_core = core_name
- best_match_len = len(map_arch)
- if best_match_core is None:
- raise ValueError(f"Could not find a matching core for arch: {arch_new}")
- return best_match_core, final_archext
- def pick_download(core: str) -> str:
- """
- Select a random DOWNLOAD type from DOWNLOAD_OPTS.
- Constraint:
- - 'ddr' is only allowed for cores containing '600' or '900' in their name
- (e.g., 'nx900', 'n600' — though only 900 appears in current map).
- """
- pool = DOWNLOAD_OPTS.copy()
- # Remove 'ddr' if core is not in 600/900 series
- if "600" not in core and "900" not in core:
- pool.remove("ddr")
- return random.choice(pool)
- def optimize_archext(archext: str) -> str:
- """
- Optimize archext string for compatibility with Nuclei Qemu.
- - Replace '_zdinx' with '_zfinx_zdinx' if present for Nuclei Qemu 2025.02.
- """
- if "_zdinx" in archext and "_zfinx" not in archext:
- archext = archext.replace("_zdinx", "_zfinx_zdinx")
- return archext
- def build_one(arch_abi: str, max_ext: int) -> Dict[str, Dict[str, str]]:
- """
- Process a single "ARCH/ABI" string (e.g., "rv32imc_zfinx/ilp32") into a full config entry.
- Steps:
- 1. Split into ARCH and ABI.
- 2. Append up to `max_ext` random extensions to ARCH.
- 3. Determine CORE and ARCH_EXT using `pick_core_archext`.
- 4. Choose DOWNLOAD type based on CORE.
- 5. Return a dict with a unique key: "{final_arch}-{download}".
- """
- if "/" not in arch_abi:
- raise ValueError(f"Malformed arch_abi: {arch_abi}")
- arch, abi = arch_abi.strip().split("/", 1)
- # Append randomly selected extensions to the base architecture string
- final_arch = arch + pick_extensions(max_ext)
- # Determine core and formatted extension string
- core, archext = pick_core_archext(final_arch)
- download = pick_download(core)
- archext = optimize_archext(archext)
- # Create a unique key for this configuration
- key = f"{final_arch}-{download}"
- return {
- key: {
- "DOWNLOAD": download,
- "CORE": core,
- "RISCV_ARCH": final_arch,
- "RISCV_ABI": abi,
- "ARCH_EXT": archext,
- }
- }
- def build_all(lines: List[str], max_ext: int) -> Dict:
- """
- Process a list of ARCH/ABI strings (one per line), skipping empty lines and comments.
- Returns a dictionary of all generated configurations.
- """
- result = {}
- for line in lines:
- line = line.strip()
- if not line or line.startswith("#"):
- continue # Skip blank lines and comments
- result.update(build_one(line, max_ext))
- return result
- # --------------------------------------------------------------------------- #
- # Command-line interface #
- # --------------------------------------------------------------------------- #
- def main(argv=None):
- """
- Main entry point. Parses arguments, reads input, generates config, and outputs JSON.
- """
- parser = argparse.ArgumentParser(
- description="Nuclei SDK CLI test-configuration generator — "
- "randomly pick CORE/DOWNLOAD/extension suffixes while "
- "respecting built-in constraints.",
- epilog=(
- "Examples:\n"
- " python gen_config.py -e 2 -p\n"
- " # Use built-in ARCH list, append 2 random extensions, pretty-print to stdout\n\n"
- " python gen_config.py -i my.list -o config.json -p\n"
- " # Read ARCH/ABI pairs from my.list, write pretty JSON to config.json\n\n"
- " cat my.list | python gen_config.py -e 1 -p\n"
- " # Read ARCH/ABI from stdin, append 1 random extension, pretty-print\n\n"
- " python gen_config.py -t custom.json -f -o full.json -e 3\n"
- " # Load custom template, generate full config with 3 random extensions\n\n"
- "Built-in rules:\n"
- " * RV64* architectures are only paired with CORE names containing 'x'.\n"
- " * RV32* architectures must use CORE names without 'x'.\n"
- " * DOWNLOAD 'ddr' is restricted to 600/900-series cores.\n"
- " * Extensions ending with '_x' are always placed last.\n"
- ),
- formatter_class=argparse.RawTextHelpFormatter,
- )
- parser.add_argument(
- "-i", "--input",
- type=str,
- help="Text file containing ARCH/ABI pairs (one per line). "
- "If omitted and stdin is empty, the built-in list is used.",
- )
- parser.add_argument(
- "-o", "--output",
- type=str,
- help="Output JSON file. If omitted, the result is printed to stdout.",
- )
- parser.add_argument(
- "-e", "--ext",
- type=int,
- default=0,
- help="Maximum number of random extensions to append (default: 0).",
- )
- parser.add_argument(
- "-p", "--pretty",
- action="store_true",
- help="Pretty-print the resulting JSON (indent 2).",
- )
- parser.add_argument(
- "-q", "--quiet",
- action="store_true",
- help="Suppress runtime messages.",
- )
- parser.add_argument(
- "-t", "--template",
- metavar="FILE",
- help="Path to custom JSON template. If omitted, a built-in template is used.",
- )
- parser.add_argument(
- "-f", "--full",
- action="store_true",
- help="Emit the full configuration object (template + build_configs) "
- "instead of only the build_configs section.",
- )
- args = parser.parse_args(argv)
- # Load configuration template (custom or default)
- if args.template:
- with open(args.template, encoding="utf-8") as f:
- template = json.load(f)
- else:
- template = DEFAULT_TEMPLATE
- # Read input ARCH/ABI list
- if args.input:
- # Read from specified file
- with open(args.input, encoding="utf-8") as f:
- lines = f.readlines()
- else:
- # If stdin has data (e.g., piped input), use it; otherwise use built-in list
- lines = sys.stdin.readlines() if not sys.stdin.isatty() else DEFAULT_ARCH_ABI
- if not args.quiet:
- print(f"/* Generating configuration for {len(lines)} ARCH/ABI entries ... */")
- # Generate all build configurations
- build_configs = build_all(lines, args.ext)
- # Decide what to output: full template or just build_configs
- if args.full:
- template["build_configs"] = build_configs
- data = template
- else:
- data = build_configs
- # Serialize to JSON
- out = json.dumps(data, indent=2) if args.pretty else json.dumps(data)
- # Write to file or stdout
- if args.output:
- with open(args.output, "w", encoding="utf-8") as f:
- f.write(out)
- print(f"/* Written to {args.output} */")
- else:
- print(out)
- if __name__ == "__main__":
- main()
|