[bazel] update `build_otbn.py` to import otbn_as module

The `build_otbn.py` was invoking the OTBN assembler Python script via
the shell, instead of importing the module directly. This does not
enable bazel make use of the hermetic python toolchain and packages.
This updates the `build_otbn.py` to invoke the OTBN assembler Python
script via a direct function call from a module import.

Signed-off-by: Timothy Trippel <ttrippel@google.com>
diff --git a/hw/ip/otbn/util/otbn_as.py b/hw/ip/otbn/util/otbn_as.py
index 3cf505a..959fd2c 100755
--- a/hw/ip/otbn/util/otbn_as.py
+++ b/hw/ip/otbn/util/otbn_as.py
@@ -1187,10 +1187,9 @@
         return 127
 
 
-def main() -> int:
-    files, other_args, flags = parse_positionals(sys.argv)
+def main(argv: List[str]) -> int:
+    files, other_args, flags = parse_positionals(argv)
     files = files or ['--']
-
     just_translate = '--otbn-translate' in flags
 
     # files is now a nonempty list of input files. Rather unusually, '--'
@@ -1246,4 +1245,4 @@
 
 
 if __name__ == '__main__':
-    sys.exit(main())
+    sys.exit(main(sys.argv))
diff --git a/meson.build b/meson.build
index 0c2d8af..f78a963 100644
--- a/meson.build
+++ b/meson.build
@@ -209,6 +209,7 @@
 prog_objdump = find_program('objdump')
 prog_objcopy = find_program('objcopy')
 
+path_otbn_tools = meson.project_source_root() / 'hw/ip/otbn/util/'
 prog_otbn_as = meson.project_source_root() / 'hw/ip/otbn/util/otbn_as.py'
 prog_otbn_ld = meson.project_source_root() / 'hw/ip/otbn/util/otbn_ld.py'
 
@@ -286,6 +287,8 @@
   'PYTHON=@0@'.format(prog_python.full_path()),
   'OTBN_AS=@0@'.format(prog_otbn_as),
   'OTBN_LD=@0@'.format(prog_otbn_ld),
+  'OTBN_TOOLS=@0@'.format(path_otbn_tools),
+  'OTBN_LD=@0@'.format(prog_otbn_ld),
   'RV32_TOOL_OBJCOPY=@0@'.format(prog_objcopy.full_path()),
   'RV32_TOOL_AS=@0@'.format(prog_as.full_path()),
   'RV32_TOOL_LD=@0@'.format(prog_ld.full_path()),
diff --git a/rules/otbn.bzl b/rules/otbn.bzl
index ab53206..2cc12e4 100644
--- a/rules/otbn.bzl
+++ b/rules/otbn.bzl
@@ -100,9 +100,8 @@
                   [ctx.executable._otbn_as] +
                   ctx.files._otbn_ld +
                   ctx.files._otbn_data +
-                  ctx.files._wrapper),
+                  [ctx.executable._wrapper]),
         env = {
-            "OTBN_AS": ctx.executable._otbn_as.path,
             "OTBN_LD": ctx.file._otbn_ld.path,
             "RV32_TOOL_AS": assembler.path,
             "RV32_TOOL_AR": cc_toolchain.ar_executable,
@@ -115,7 +114,7 @@
             "--no-assembler",
             "--out-dir={}".format(elf.dirname),
         ] + [obj.path for obj in (objs + deps)],
-        executable = ctx.file._wrapper,
+        executable = ctx.executable._wrapper,
     )
 
     feature_configuration = cc_common.configure_features(
@@ -189,8 +188,9 @@
             allow_files = True,
         ),
         "_wrapper": attr.label(
-            default = "//util:otbn_build.py",
-            allow_single_file = True,
+            default = "//util:otbn_build",
+            executable = True,
+            cfg = "exec",
         ),
     },
     fragments = ["cpp"],
diff --git a/sw/device/lib/crypto/meson.build b/sw/device/lib/crypto/meson.build
index 284cd64..a9f28ee 100644
--- a/sw/device/lib/crypto/meson.build
+++ b/sw/device/lib/crypto/meson.build
@@ -25,7 +25,7 @@
 
 otbn_build_command = [
   prog_env,
-  'OTBN_AS=@0@'.format(prog_otbn_as),
+  'OTBN_TOOLS=@0@'.format(path_otbn_tools),
   'OTBN_LD=@0@'.format(prog_otbn_ld),
   'RV32_TOOL_OBJCOPY=@0@'.format(prog_objcopy.full_path()),
   'RV32_TOOL_AS=@0@'.format(prog_as.full_path()),
diff --git a/sw/otbn/meson.build b/sw/otbn/meson.build
index 2540e38..0b0a034 100644
--- a/sw/otbn/meson.build
+++ b/sw/otbn/meson.build
@@ -45,7 +45,7 @@
 
 otbn_build_command = [
   prog_env,
-  'OTBN_AS=@0@'.format(prog_otbn_as),
+  'OTBN_TOOLS=@0@'.format(path_otbn_tools),
   'OTBN_LD=@0@'.format(prog_otbn_ld),
   'RV32_TOOL_OBJCOPY=@0@'.format(prog_objcopy.full_path()),
   'RV32_TOOL_AS=@0@'.format(prog_as.full_path()),
diff --git a/util/BUILD b/util/BUILD
index c02957a..34188a1 100644
--- a/util/BUILD
+++ b/util/BUILD
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 load("@rules_python//python:defs.bzl", "py_binary")
+load("@ot_python_deps//:requirements.bzl", "requirement")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -16,6 +17,16 @@
 )
 
 py_binary(
+    name = "otbn_build",
+    srcs = ["otbn_build.py"],
+    imports = ["../hw/ip/otbn/util/"],
+    deps = [
+        requirement("pyelftools"),
+        "//hw/ip/otbn/util:otbn_as",
+    ],
+)
+
+py_binary(
     name = "rom_chip_info",
     srcs = ["rom_chip_info.py"],
 )
diff --git a/util/otbn_build.py b/util/otbn_build.py
index 4453095..3e2d14d 100755
--- a/util/otbn_build.py
+++ b/util/otbn_build.py
@@ -15,7 +15,7 @@
   sensible default values are provided (tools are generally expected to be in
   the $PATH).
 
-  OTBN_AS            path to otbn_as.py, the OTBN assembler
+  OTBN_TOOLS         path to the OTBN linker and assemler tools
   OTBN_LD            path to otbn_ld.py, the OTBN linker
   RV32_TOOL_LD       path to RV32 ld
   RV32_TOOL_AS       path to RV32 as
@@ -48,6 +48,17 @@
 
 REPO_TOP = Path(__file__).parent.parent.resolve()
 
+# yapf: disable
+
+# TODO: remove with meson; bazel will set the PYTHONPATH to locate otbn
+# tool modules
+otbn_tools_path = os.environ.get('OTBN_TOOLS', None)
+if otbn_tools_path:
+    sys.path.append(otbn_tools_path)
+import otbn_as
+
+# yapf: enable
+
 
 def cmd_to_str(cmd: List[str]) -> str:
     return ' '.join([shlex.quote(str(a)) for a in cmd])
@@ -68,7 +79,7 @@
     subprocess.run(str_args, check=True)
 
 
-def run_tool(tool: str, out_file: Path, args) -> None:
+def run_tool(tool, out_file: Path, args) -> None:
     '''Run tool to produce out_file (using an '-o' argument)
 
     This works by writing to a temporary file (in the same directory) and then
@@ -83,8 +94,11 @@
                                           dir=out_dir,
                                           delete=False)
     try:
-        run_cmd([tool, '-o', tmpfile.name] + args,
-                cmd_to_str([tool, '-o', out_file] + args))
+        if type(tool) == str:
+            run_cmd([tool, '-o', tmpfile.name] + args,
+                    cmd_to_str([tool, '-o', out_file] + args))
+        else:
+            tool(['', '-o', tmpfile.name] + list(map(str, args)))
 
         # If we get here, the tool ran successfully, producing the output file.
         # Use os.replace to rename appropriately.
@@ -101,9 +115,7 @@
 
 
 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.py'))
-    run_tool(otbn_as_cmd, out_file, [src_file])
+    run_tool(otbn_as.main, out_file, [src_file])
 
 
 def call_otbn_ld(src_files: List[Path], out_file: Path,