[sw/otbn] Integrate OTBN code building into meson

Build OTBN code with the meson build system. The primary use case is the
integration of OTBN binaries into device (Ibex) binaries.

OTBN is a separate architecture based on RV32I. Since meson doesn't
support compiling (and combining) software for multiple targets in the
same run, we have to cheat by using an external script, which
effectively does what meson would do for "normal" device software: call
the assembler, linker, and binutils to produce the desired output files
from the sources.

However, since we know that OTBN is based on RV32I, we make at least
sure that we reuse the toolchain discovery results from meson and pass
them on to the OTBN toolchain. The OTBN toolchain itself is a "frontend"
to the GNU Assembler and the GNU linker, callable through otbn-as and
otbn-ld.

Fixes #2936

Signed-off-by: Philipp Wagner <phw@lowrisc.org>
diff --git a/meson.build b/meson.build
index ffa40b2..cc77935 100644
--- a/meson.build
+++ b/meson.build
@@ -147,6 +147,8 @@
 
 # Common program references.
 prog_python = import('python').find_installation('python3')
+prog_ld = find_program('ld')
+prog_as = find_program('as')
 prog_objdump = find_program('objdump')
 prog_objcopy = find_program('objcopy')
 prog_srec_cat = find_program('srec_cat')
diff --git a/sw/meson.build b/sw/meson.build
index 5b42f24..bbc4fdd 100644
--- a/sw/meson.build
+++ b/sw/meson.build
@@ -12,5 +12,6 @@
 ]
 
 subdir('vendor')
+subdir('otbn')
 subdir('device')
 subdir('host')
diff --git a/sw/otbn/code-snippets/meson.build b/sw/otbn/code-snippets/meson.build
new file mode 100644
index 0000000..ac7d861
--- /dev/null
+++ b/sw/otbn/code-snippets/meson.build
@@ -0,0 +1,32 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+sw_otbn_sources += {
+  'barrett384': files(
+    'barrett384.s',
+  ),
+  'modexp': files(
+    'modexp.s'
+  ),
+  'loop': files(
+    'loop.s',
+  ),
+  'mul256': files(
+    'mul256.s',
+  ),
+  'mul384': files(
+    'mul384.s',
+  ),
+  'pseudo-ops': files(
+    'pseudo-ops.s',
+  ),
+  'rsa_1024_dec_test': files(
+    'rsa_1024_dec_test.s',
+    'modexp.s',
+  ),
+  'rsa_1024_enc_test': files(
+    'rsa_1024_enc_test.s',
+    'modexp.s',
+  ),
+}
diff --git a/sw/otbn/meson.build b/sw/otbn/meson.build
new file mode 100644
index 0000000..2ca219c
--- /dev/null
+++ b/sw/otbn/meson.build
@@ -0,0 +1,103 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# Build definitions for OTBN software
+#
+# OTBN software is built with a separate toolchain, which is called as an
+# external target from Meson. All functionality to call the external toolchain
+# is encapsulated in this build file; users only need to care about two
+# dictionaries.
+#
+# To use an OTBN application in device software, use the sw_otbn dictionary.
+# For example, to add a dependency to an embeddable version of the barrett384
+# OTBN application, add the following variable to the list of dependencies:
+# sw_otbn['barrett384']['rv32embed_dependency']
+#
+# The otbn_sw dictionary has the following structure:
+# otbn_sw = {
+#   APPNAME: {
+#     'elf': OTBN_ELF_FILE,
+#     'rv32embed_lib': RV32_LIBRARY,
+#     'rv32embed_dependency': DEPENDENCY_ON_RV32_LIBRARY,
+# }}
+#
+# To add another OTBN application to the list of build targets, add it to
+# the sw_otbn_sources dictionary. The existing definitions should be a good
+# example of how to do that.
+# Structure:
+# sw_otbn_sources = { appname: files(source1, source2, ), ... }
+#
+# Note that application names must be unique across subdirectories.
+# The directory structure below sw/otbn is not preserved, all build output is
+# in sw/otbn. (Preserving the subdirectories seems to be impossible with
+# custom_target() not accepting paths but only file names for its `output` key.)
+
+# All OTBN software is added to this dictionary and then built in one go.
+sw_otbn_sources = {}
+
+# All subdirectories add the objects they want to build to the sw_otbn_sources
+# dictionary.
+subdir('code-snippets')
+
+prog_otbn_as = meson.source_root() / 'hw/ip/otbn/util/otbn-as'
+prog_otbn_ld = meson.source_root() / 'hw/ip/otbn/util/otbn-ld'
+
+prog_env = find_program('env')
+prog_otbn_build = meson.source_root() / 'util/otbn_build.py'
+
+otbn_build_command = [
+  prog_env,
+  'OTBN_AS=@0@'.format(prog_otbn_as),
+  'OTBN_LD=@0@'.format(prog_otbn_ld),
+  'RV32_TOOL_OBJCOPY=@0@'.format(prog_objcopy.path()),
+  'RV32_TOOL_AS=@0@'.format(prog_as.path()),
+  'RV32_TOOL_LD=@0@'.format(prog_ld.path()),
+  prog_python,
+  prog_otbn_build,
+  '--out-dir',
+  '@OUTDIR@',
+  '@INPUT@',
+]
+
+# Note on variable naming below: Variables in meson are global, we hence prefix
+# all variables with sw_otbn as "our namespace". Variables which are meant to be
+# local to this file are prefixed with `sw_otbn__`.
+
+sw_otbn = {}
+foreach sw_otbn__app_name, sw_otbn__app_sources : sw_otbn_sources
+  # Output files generated by the otbn_build.py script.
+  sw_otbn__app_output_files = [
+    sw_otbn__app_name + '.rv32embed.o',
+    sw_otbn__app_name + '.elf',
+  ]
+
+  # Target calling otbn_build.py
+  sw_otbn__target = custom_target(
+    'sw_otbn_codesnippets_apps_' + sw_otbn__app_name + '_target',
+    input: sw_otbn__app_sources,
+    output: sw_otbn__app_output_files,
+    command: otbn_build_command,
+  )
+
+  # A library containing the OTBN application in a form embeddable into device
+  # (Ibex) software (the *.rv32embed.o file).
+  sw_otbn__embedded_lib = static_library(
+    sw_otbn__app_name,
+    [sw_otbn__target[0]] # == sw_otbn__app_output_files[0], i.e. *.rv32embed.o
+  )
+
+  # A dependency on the application as embeddable library, to be used if
+  # device (Ibex) software wants to include an OTBN application in its binary.
+  sw_otbn__dependency = declare_dependency(
+    link_with: sw_otbn__embedded_lib,
+  )
+
+  sw_otbn += {
+    sw_otbn__app_name: {
+      'elf': sw_otbn__target[1],
+      'rv32embed_lib': sw_otbn__embedded_lib,
+      'rv32embed_dependency': sw_otbn__dependency,
+    }
+  }
+endforeach
diff --git a/util/otbn_build.py b/util/otbn_build.py
new file mode 100755
index 0000000..42da512
--- /dev/null
+++ b/util/otbn_build.py
@@ -0,0 +1,134 @@
+#!/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
+
+"""Build software running on OTBN
+
+Each assembly source file is first assembled with otbn-as. All resulting objects
+are then linked with otbn-ld. The resulting ELF file is converted into an
+embeddable RV32 object file using objcopy.  In this object, all symbols
+are prefixed with `_otbn_app_<appname>_` (only global symbols are included).
+
+environment variables:
+  This script, and the tools called from it, rely on the following environment
+  variables for configuration. All environment variables are optional, and
+  sensible default values are provided (tools are generally expected to be in
+  the $PATH).
+
+  OTBN_AS            path to otbn-as, the OTBN assembler
+  OTBN_LD            path to otbn-ld, the OTBN linker
+  RV32_TOOL_LD       path to RV32 ld
+  RV32_TOOL_AS       path to RV32 as
+  RV32_TOOL_OBJCOPY  path to RV32 objcopy
+
+  The RV32* environment variables are used by both this script and the OTBN
+  wrappers (otbn-as and otbn-ld) to find tools in a RV32 toolchain.
+
+outputs:
+  The build process produces multiple files inside the output directory.
+
+  <src_file>.o            the compiled source files
+  <app_name>.elf          the compiled and linked application targeting OTBN
+  <app_name>.rv32embed.o  the application as embeddable object for RV32
+
+"""
+
+import argparse
+import logging as log
+import os
+import shlex
+import subprocess
+import sys
+from pathlib import Path
+from typing import List
+
+log.basicConfig(level=log.INFO, format="%(message)s")
+
+REPO_TOP = Path(__file__).parent.parent.resolve()
+
+def cmd_to_str(cmd):
+    return ' '.join([shlex.quote(str(a)) for a in cmd])
+
+
+def run_cmd(cmd, **kwargs):
+    log.info(cmd_to_str(cmd))
+    subprocess.run(cmd, **kwargs)
+
+
+def call_otbn_as(src_file: Path, out_file: Path):
+    otbn_as_cmd = os.environ.get('OTBN_AS',
+                                 str(REPO_TOP / 'hw/ip/otbn/util/otbn-as'))
+    run_cmd([otbn_as_cmd, '-o', out_file, src_file], check=True)
+
+
+def call_otbn_ld(src_files: List[Path], out_file: Path):
+    otbn_ld_cmd = os.environ.get('OTBN_LD',
+                                 str(REPO_TOP / 'hw/ip/otbn/util/otbn-ld'))
+    run_cmd([otbn_ld_cmd, '-o', out_file] + src_files, check=True)
+
+
+def call_rv32_objcopy(args: List[str]):
+    rv32_tool_objcopy = os.environ.get('RV32_TOOL_OBJCOPY',
+                                       'riscv32-unknown-elf-objcopy')
+    run_cmd([rv32_tool_objcopy] + args, check=True)
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument(
+        '--out-dir',
+        '-o',
+        required=False,
+        default=".",
+        help="Output directory (default: %(default)s)")
+    parser.add_argument(
+        '--app-name',
+        '-n',
+        required=False,
+        help="Name of the application, used as basename for the output. "
+             "Default: basename of the first source file.")
+    parser.add_argument('src_files', nargs='+', type=str, metavar='SRC_FILE')
+    args = parser.parse_args()
+
+    out_dir = Path(args.out_dir)
+    out_dir.mkdir(exist_ok=True)
+
+    src_files = [Path(f) for f in args.src_files]
+    for src_file in src_files:
+        if not src_file.exists():
+            log.fatal("Source file %s not found." % src_file)
+            return 1
+    obj_files = [out_dir / f.with_suffix('.o').name for f in src_files]
+
+    app_name = args.app_name or str(src_files[0].stem)
+
+    try:
+        for src_file, obj_file in zip(src_files, obj_files):
+            call_otbn_as(src_file, obj_file)
+
+        out_elf = out_dir / (app_name + '.elf')
+        call_otbn_ld(obj_files, out_elf)
+
+        out_embedded_obj = out_dir / (app_name + '.rv32embed.o')
+        args = [
+            '-O',
+            'elf32-littleriscv',
+            '--prefix-symbols',
+            '_otbn_app_' + app_name + '_',
+            out_elf,
+            out_embedded_obj,
+        ]
+
+        call_rv32_objcopy(args)
+    except subprocess.CalledProcessError as e:
+        # Show a nicer error message if any of the called programs fail.
+        log.fatal("Command {!r} returned non-zero exit code {}".format(
+            cmd_to_str(e.cmd), e.returncode))
+        return 1
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())