[otbn] A script to generate binaries for UVM tests
We still haven't figured out how to wire this up to the dvsim flow,
but this script does the "make me some binaries" step.
An example of how to use it (slightly complicated because of dvsim's
per-branch directory scheme):
cur_branch="$(git rev-parse --abbrev-ref HEAD)"
sw_dest_dir="scratch/otbn.sim.vcs/$cur_branch/sw"
hw/ip/otbn/dv/uvm/gen-binaries.py --seed 1234 "$sw_dest_dir"
util/dvsim/dvsim.py hw/ip/otbn/dv/uvm/otbn_sim_cfg.hjson -i otbn_single
Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/uvm/gen-binaries.py b/hw/ip/otbn/dv/uvm/gen-binaries.py
new file mode 100755
index 0000000..a29be94
--- /dev/null
+++ b/hw/ip/otbn/dv/uvm/gen-binaries.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''A helper script to generate a default set of binaries for OTBN testing
+
+This is intended for use with dvsim, which should call this script as
+part of the test build phase.
+
+'''
+
+import argparse
+import os
+import subprocess
+import sys
+from typing import Dict, List, Optional, TextIO, Union
+
+
+def read_positive(val: str) -> int:
+ ival = -1
+ try:
+ ival = int(val, 0)
+ except ValueError:
+ pass
+
+ if ival <= 0:
+ raise argparse.ArgumentTypeError('{!r} is not a positive integer.'
+ .format(val))
+ return ival
+
+
+def read_jobs(val: Optional[str]) -> Optional[Union[str, int]]:
+ if val == 'unlimited':
+ return 'unlimited'
+ if val is None:
+ return None
+
+ return read_positive(val)
+
+
+class Toolchain:
+ def __init__(self, env_data: Dict[str, str]) -> None:
+ self.otbn_as = self.get_tool(env_data, 'OTBN_AS')
+ self.otbn_ld = self.get_tool(env_data, 'OTBN_LD')
+ self.rv32_tool_as = self.get_tool(env_data, 'RV32_TOOL_AS')
+ self.rv32_tool_ld = self.get_tool(env_data, 'RV32_TOOL_LD')
+
+ @staticmethod
+ def get_tool(env_data: Dict[str, str], tool: str) -> str:
+ path = env_data.get(tool)
+ if path is None:
+ raise RuntimeError('No entry for {} in .env file'.format(tool))
+ return path
+
+ def run(self, cmd: List[str]) -> None:
+ '''Wrapper around subprocess.run that sets needed environment vars'''
+ env = os.environ.copy()
+ env['RV32_TOOL_AS'] = self.rv32_tool_as
+ env['RV32_TOOL_LD'] = self.rv32_tool_ld
+ subprocess.run(cmd, env=env)
+
+
+def take_env_line(dst: Dict[str, str],
+ path: str, line_number: int, line: str) -> None:
+ '''Read one line from a .env file, updating dst.'''
+ line = line.split('#', 1)[0].strip()
+ if not line:
+ return
+
+ parts = line.split('=', 1)
+ if len(parts) != 2:
+ raise RuntimeError('{}:{}: No equals sign in line {!r}.'
+ .format(path, line_number, line))
+
+ dst[parts[0]] = parts[1]
+
+
+def read_toolchain(obj_dir_arg: Optional[str], otbn_dir: str) -> Toolchain:
+ '''Read Meson's dumped .env file to get toolchain info'''
+ if obj_dir_arg is not None:
+ obj_dir = obj_dir_arg
+ source = 'specified by an --obj-dir argument'
+ else:
+ obj_dir_env = os.getenv('OBJ_DIR')
+ if obj_dir_env is not None:
+ obj_dir = obj_dir_env
+ source = ('inferred from OBJ_DIR environment variable; '
+ 'have you run meson_init.sh?')
+ else:
+ git_dir = os.path.normpath(os.path.join(otbn_dir, '../' * 3))
+ obj_dir = os.path.normpath(os.path.join(git_dir, 'build-out'))
+ source = ('inferred from script location; '
+ 'have you run meson_init.sh?')
+
+ env_path = os.path.join(obj_dir, '.env')
+ try:
+ with open(env_path) as env_file:
+ env_dict = {} # type: Dict[str, str]
+ for idx, line in enumerate(env_file):
+ take_env_line(env_dict, env_path, idx + 1, line)
+
+ return Toolchain(env_dict)
+ except OSError as ose:
+ raise RuntimeError('Failed to read .env file at {!r} '
+ '(at a path {}): {}.'
+ .format(env_path, source, ose)) from None
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--count', type=read_positive, default=10,
+ help='Number of binaries to generate')
+ parser.add_argument('--obj-dir',
+ help=('Object directory configured with Meson (used '
+ 'to find tool configuration). If not supplied, '
+ 'defaults to the OBJ_DIR environment variable '
+ 'if set, or build-out at the top of the '
+ 'repository if not.'))
+ parser.add_argument('--seed', type=read_positive, default=0)
+ parser.add_argument('--verbose', '-v', action='store_true')
+ parser.add_argument('--jobs', '-j', type=read_jobs, nargs='?',
+ const='unlimited', help='Number of parallel jobs.')
+ parser.add_argument('destdir',
+ help='Destination directory')
+
+ args = parser.parse_args()
+
+ script_dir = os.path.dirname(__file__)
+ otbn_dir = os.path.normpath(os.path.join(script_dir, '../' * 2))
+
+ try:
+ toolchain = read_toolchain(args.obj_dir, otbn_dir)
+ except RuntimeError as err:
+ print(err, file=sys.stderr)
+ return 1
+
+ rig_count = args.count - 1
+
+ os.makedirs(args.destdir, exist_ok=True)
+
+ with open(os.path.join(args.destdir, 'build.ninja'), 'w') as ninja_handle:
+ write_ninja(ninja_handle, rig_count, args.seed, toolchain, otbn_dir)
+
+ # Handle the -j argument like Make does, defaulting to 1 thread. This
+ # behaves a bit more reasonably than ninja's default (# cores) if we're
+ # running underneath something else.
+ if args.jobs is None:
+ j_arg = 1
+ elif args.jobs == 'unlimited':
+ j_arg = 0
+ else:
+ j_arg = args.jobs
+
+ cmd = ['ninja', '-j', str(j_arg)]
+ if args.verbose:
+ cmd.append('-v')
+
+ return subprocess.run(cmd, cwd=args.destdir, check=False).returncode
+
+
+def write_ninja(handle: TextIO, rig_count: int, start_seed: int,
+ toolchain: Toolchain, otbn_dir: str) -> None:
+ '''Write a build.ninja to build rig_count random binaries and a smoke test
+
+ The rules build everything in the same directory as the build.ninja file.
+ OTBN tooling is found in util_dir.
+
+ '''
+ otbn_rig = os.path.join(otbn_dir, 'util/otbn-rig')
+ smoke_src_dir = os.path.join(otbn_dir, 'dv/smoke')
+
+ seeds = [start_seed + idx for idx in range(rig_count)]
+
+ handle.write('rule rig\n'
+ ' command = {rig} gen --size 100 --seed $seed |'
+ ' {rig} asm -o $seed\n'
+ .format(rig=otbn_rig))
+
+ handle.write('rule as\n'
+ ' command = RV32_TOOL_AS={rv32_as} {otbn_as} -o $out $in\n'
+ .format(rv32_as=toolchain.rv32_tool_as,
+ otbn_as=toolchain.otbn_as))
+
+ handle.write('rule ld\n'
+ ' command = RV32_TOOL_LD={rv32_ld} '
+ '{otbn_ld} -o $out -T $ldscript $in\n'
+ .format(rv32_ld=toolchain.rv32_tool_ld,
+ otbn_ld=toolchain.otbn_ld))
+
+ handle.write('rule ld1\n'
+ ' command = {otbn_ld} -o $out $in\n\n'
+ .format(otbn_ld=toolchain.otbn_ld))
+
+ for seed in seeds:
+ # Generate the .s and .ld files.
+ handle.write('build {seed}.s {seed}.ld: rig\n'
+ ' seed = {seed}\n'
+ .format(seed=seed))
+
+ # Assemble the asm file to an object
+ handle.write('build {seed}.o: as {seed}.s\n'.format(seed=seed))
+
+ # Link the object to an ELF, using the relevant LD file
+ handle.write('build {seed}.elf: ld {seed}.o\n'
+ ' ldscript = {seed}.ld\n\n'
+ .format(seed=seed))
+
+ # Rules to build the smoke test.
+ smoke_src = os.path.join(os.path.abspath(smoke_src_dir), 'smoke_test.s')
+ handle.write('build smoke.o: as {smoke_src}\n'.format(smoke_src=smoke_src))
+ handle.write('build smoke.elf: ld1 smoke.elf\n\n')
+
+
+if __name__ == '__main__':
+ sys.exit(main())