| #!/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('--size', type=read_positive, default=100) |
| 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, args.size, 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, size: 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. |
| |
| ''' |
| assert start_seed >= 0 |
| assert size > 0 |
| |
| otbn_rig = os.path.join(otbn_dir, 'dv/rig/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-gen\n' |
| ' command = {rig} gen --size {size} --seed $seed >$out\n' |
| .format(rig=otbn_rig, size=size)) |
| |
| handle.write('rule rig-asm\n' |
| ' command = {rig} asm -o $seed $in\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 = RV32_TOOL_LD={rv32_ld} {otbn_ld} -o $out $in\n\n' |
| .format(rv32_ld=toolchain.rv32_tool_ld, |
| otbn_ld=toolchain.otbn_ld)) |
| |
| for seed in seeds: |
| # Generate the .s and .ld files. |
| handle.write('build {seed}.json: rig-gen\n' |
| ' seed = {seed}\n' |
| .format(seed=seed)) |
| |
| handle.write('build {seed}.s {seed}.ld: rig-asm {seed}.json\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.o\n\n') |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |