blob: 47eb8ec65adb9a3adc167a5bbfc3d994562ba8cd [file] [log] [blame]
#!/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)
def is_exe(path: str) -> bool:
return os.path.isfile(path) and os.access(path, os.X_OK)
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('Unable to find tool: {}.'.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 get_toolchain(otbn_dir: str) -> Toolchain:
'''Reads environment variables to get toolchain info.'''
env_dict = {} # type: Dict[str, str]
# OTBN assembler and linker
env_dict['OTBN_AS'] = f"{otbn_dir}/util/otbn_as.py"
env_dict['OTBN_LD'] = f"{otbn_dir}/util/otbn_ld.py"
# RV32 assembler and linker
env_dict['RV32_TOOL_AS'] = os.getenv('RV32_TOOL_AS')
rv32_tool_as_default = "tools/riscv/bin/riscv32-unknown-elf-as"
if env_dict['RV32_TOOL_AS'] is None and is_exe(rv32_tool_as_default):
env_dict['RV32_TOOL_AS'] = rv32_tool_as_default
env_dict['RV32_TOOL_LD'] = os.getenv('RV32_TOOL_LD')
rv32_tool_ld_default = "tools/riscv/bin/riscv32-unknown-elf-ld"
if env_dict['RV32_TOOL_LD'] is None and is_exe(rv32_tool_ld_default):
env_dict['RV32_TOOL_LD'] = rv32_tool_ld_default
return Toolchain(env_dict)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument('--count',
type=read_positive,
help='Number of binaries to generate (default: 10)')
parser.add_argument('--seed', type=read_positive)
parser.add_argument('--size', type=read_positive)
parser.add_argument('--src-dir',
help=('If supplied, gen-binaries.py will not generate '
'random binaries. Instead, it will assemble and '
'link each .s file that it can find in the '
'given directory. This is useful for building '
'the smoke test or other directed tests.'))
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('--gen-only',
action='store_true',
help="Generate the ninja file but don't run it")
parser.add_argument('--ninja-suffix', type=str)
parser.add_argument('destdir', help='Destination directory')
args = parser.parse_args()
# Argument consistency checks
if args.src_dir is None:
if args.count is None:
args.count = 10
if args.seed is None:
args.seed = 1
if args.size is None:
args.size = 100
else:
if args.count is not None:
raise RuntimeError('Invalid combination: --count and --src-dir '
'both supplied.')
if args.seed is not None:
raise RuntimeError('Invalid combination: --seed and --src-dir '
'both supplied.')
if args.size is not None:
raise RuntimeError('Invalid combination: --size and --src-dir '
'both supplied.')
script_dir = os.path.dirname(__file__)
otbn_dir = os.path.normpath(os.path.join(script_dir, '../' * 2))
try:
toolchain = get_toolchain(otbn_dir)
except RuntimeError as err:
print(err, file=sys.stderr)
return 1
os.makedirs(args.destdir, exist_ok=True)
ninja_fname = 'build.ninja'
if args.ninja_suffix is not None:
ninja_fname += '.' + args.ninja_suffix
with open(os.path.join(args.destdir, ninja_fname), 'w') as ninja_handle:
if args.src_dir is None:
write_ninja_rnd(ninja_handle, toolchain, otbn_dir, args.count,
args.seed, args.size)
else:
write_ninja_fixed(ninja_handle, toolchain, otbn_dir, args.src_dir)
if args.gen_only:
return 0
# 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_rnd(handle: TextIO, toolchain: Toolchain, otbn_dir: str,
count: int, start_seed: int, size: int) -> None:
'''Write a build.ninja to build random binaries.
The rules build everything in the same directory as the build.ninja file.
OTBN tooling is found through the toolchain argument.
'''
assert count > 0
assert start_seed >= 0
assert size > 0
otbn_rig = os.path.join(otbn_dir, 'dv/rig/otbn-rig')
handle.write(
'rule rig-gen\n'
' command = {rig} gen --size {size} --seed $seed -o $out\n\n'.format(
rig=otbn_rig, size=size))
handle.write('rule rig-asm\n'
' command = {rig} asm -o $seed $in\n\n'.format(rig=otbn_rig))
handle.write(
'rule as\n'
' command = RV32_TOOL_AS={rv32_as} {otbn_as} -o $out $in\n\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))
for seed in range(start_seed, start_seed + count):
# 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))
def write_ninja_fixed(handle: TextIO, toolchain: Toolchain, otbn_dir: str,
src_dir: str) -> None:
'''Write a build.ninja to build a fixed set of binaries
The rules build everything in the same directory as the build.ninja file.
OTBN tooling is found through the toolchain argument.
'''
handle.write(
'rule as\n'
' command = RV32_TOOL_AS={rv32_as} {otbn_as} -o $out $in\n\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 $in\n\n'.format(
rv32_ld=toolchain.rv32_tool_ld, otbn_ld=toolchain.otbn_ld))
count = 0
for fname in os.listdir(src_dir):
if not fname.endswith('.s'):
continue
abs_path = os.path.abspath(os.path.join(src_dir, fname))
basename = fname[:-2]
handle.write(f'build {basename}.o: as {abs_path}\n')
handle.write(f'build {basename}.elf: ld {basename}.o\n\n')
count += 1
if not count:
raise RuntimeError(f'No .s files in {src_dir}')
if __name__ == '__main__':
sys.exit(main())