| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| load("//rules:rv.bzl", "rv_rule") |
| load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain") |
| |
| def _get_assembler(cc_toolchain): |
| """Find the path to riscv-unknown-elf-as.""" |
| |
| # Note: the toolchain config doesn"t appear to have a good way to get |
| # access to the assembler. We should be able to access it via the |
| # the compiler, but I had trouble with //hw/ip/otbn/util/otbn_as.py invoking |
| # the compiler as assembler. |
| return [f for f in cc_toolchain.all_files.to_list() if f.basename.endswith("as")][0] |
| |
| def _otbn_assemble_sources(ctx): |
| """Helper function that, for each source file in the provided context, adds |
| an action to the context that invokes the otbn assember (otbn_as.py), |
| producing a corresponding object file. Returns a list of all object files |
| that will be generated by these actions. |
| """ |
| cc_toolchain = find_cc_toolchain(ctx).cc |
| assembler = _get_assembler(cc_toolchain) |
| |
| objs = [] |
| for src in ctx.files.srcs: |
| obj = ctx.actions.declare_file(src.basename.replace("." + src.extension, ".o")) |
| objs.append(obj) |
| ctx.actions.run( |
| outputs = [obj], |
| inputs = ([src] + |
| cc_toolchain.all_files.to_list() + |
| [ctx.executable._otbn_as]), |
| env = { |
| "RV32_TOOL_AS": assembler.path, |
| }, |
| arguments = ["-o", obj.path, src.path], |
| executable = ctx.executable._otbn_as, |
| ) |
| |
| return objs |
| |
| def _otbn_library(ctx): |
| """Produces a collection of object files, one per source file, that can be |
| used as a dependency for otbn binaries.""" |
| objs = _otbn_assemble_sources(ctx) |
| |
| return [ |
| DefaultInfo( |
| files = depset(objs), |
| data_runfiles = ctx.runfiles(files = objs), |
| ), |
| ] |
| |
| def _otbn_binary(ctx): |
| """The build process for otbn resources currently invokes |
| `//hw/ip/otbn/util/otbn_{as,ld,...}.py` to build the otbn resource. |
| These programs are python scripts which translate otbn special |
| instructions into the proper opcode sequences and _then_ invoke the normal |
| `rv32-{as,ld,...}` programs to produce the resource. These "native" |
| otbn resources are the `otbn_objs` and `elf` output groups. |
| |
| In order to make the otbn resource useful to the the main CPU, the |
| otbn resource needs to be included as a blob of data that the main |
| CPU can dump into the otbn `imem` area and ask otbn to execute it. |
| `util/otbn-build.py` does this with some objcopy-fu, emitting |
| `foo.rv32embed.o`. Bazel's `cc_*` rules really want dependency objects |
| expressed as archives rather than raw object files, so I've modified |
| `otbn-build` to also emit an archive file. |
| |
| _Morally_, the otbn resource is a data dependency. However the |
| practical meaning of a `data` dependency in bazel is a file made |
| available at runtime, which is not how we're using the otbn resource. |
| The closest analog is something like `cc_embed_data`, which is like |
| a data dependency that needs to be linked into the main program. |
| We achieve by having `otbn_build.py` emit a conventional RV32I library |
| that other rules can depend on in their `deps`. |
| """ |
| cc_toolchain = find_cc_toolchain(ctx).cc |
| assembler = _get_assembler(cc_toolchain) |
| |
| # Run the otbn assembler on source files to produce object (.o) files. |
| objs = _otbn_assemble_sources(ctx) |
| |
| # Declare output files. |
| elf = ctx.actions.declare_file(ctx.attr.name + ".elf") |
| rv32embed = ctx.actions.declare_file(ctx.attr.name + ".rv32embed.o") |
| archive = ctx.actions.declare_file(ctx.attr.name + ".rv32embed.a") |
| |
| deps = [f for dep in ctx.attr.deps for f in dep.files.to_list()] |
| |
| # Run the otbn_build.py script to link object files from the sources and |
| # dependencies. |
| ctx.actions.run( |
| outputs = [elf, rv32embed, archive], |
| inputs = (objs + |
| deps + |
| cc_toolchain.all_files.to_list() + |
| ctx.files._otbn_data + |
| [ctx.executable._wrapper]), |
| env = { |
| "RV32_TOOL_AS": assembler.path, |
| "RV32_TOOL_AR": cc_toolchain.ar_executable, |
| "RV32_TOOL_LD": cc_toolchain.ld_executable, |
| "RV32_TOOL_OBJCOPY": cc_toolchain.objcopy_executable, |
| }, |
| arguments = [ |
| "--app-name={}".format(ctx.attr.name), |
| "--archive", |
| "--no-assembler", |
| "--out-dir={}".format(elf.dirname), |
| ] + [obj.path for obj in (objs + deps)], |
| executable = ctx.executable._wrapper, |
| ) |
| |
| feature_configuration = cc_common.configure_features( |
| ctx = ctx, |
| cc_toolchain = cc_toolchain, |
| requested_features = ctx.features, |
| unsupported_features = ctx.disabled_features, |
| ) |
| |
| outputs = objs + [elf, rv32embed, archive] |
| return [ |
| DefaultInfo(files = depset(outputs), data_runfiles = ctx.runfiles(files = outputs)), |
| OutputGroupInfo( |
| otbn_objs = depset(objs + deps), |
| elf = depset([elf]), |
| rv32embed = depset([rv32embed]), |
| archive = depset([archive]), |
| ), |
| # Emit a CcInfo provider so that this rule can be a dependency in other |
| # cc_* rules. |
| CcInfo( |
| linking_context = cc_common.create_linking_context( |
| linker_inputs = depset([cc_common.create_linker_input( |
| owner = ctx.label, |
| libraries = depset([cc_common.create_library_to_link( |
| actions = ctx.actions, |
| feature_configuration = feature_configuration, |
| cc_toolchain = cc_toolchain, |
| static_library = archive, |
| )]), |
| )]), |
| ), |
| ), |
| ] |
| |
| def _otbn_sim_test(ctx): |
| """This rule is for standalone OTBN unit tests, which are run on the host |
| via the OTBN simulator. |
| |
| It first generates binaries using the same method as otbn_binary, then runs |
| them on the simulator. Tests are expected to count failures in the w0 |
| register; the test checks that w0=0 to determine if the test passed. |
| """ |
| providers = _otbn_binary(ctx) |
| |
| # Extract the output .elf file from the output group. |
| elf = providers[1].elf.to_list()[0] |
| |
| exp_file = ctx.file.exp |
| |
| # Create a simple script that runs the OTBN test wrapper on the .elf file |
| # using the provided simulator path. |
| sim_test_wrapper = ctx.executable._sim_test_wrapper |
| simulator = ctx.executable._simulator |
| ctx.actions.write( |
| output = ctx.outputs.executable, |
| content = "{} {} {} {}".format(sim_test_wrapper.short_path, simulator.short_path, exp_file.short_path, elf.short_path), |
| ) |
| |
| # Runfiles include sources, the .elf file, the simulator and test wrapper |
| # themselves, and all the simulator and test wrapper runfiles. |
| runfiles = ctx.runfiles(files = (ctx.files.srcs + [elf, exp_file, ctx.executable._simulator, ctx.executable._sim_test_wrapper])) |
| runfiles = runfiles.merge(ctx.attr._simulator[DefaultInfo].default_runfiles) |
| runfiles = runfiles.merge(ctx.attr._sim_test_wrapper[DefaultInfo].default_runfiles) |
| return [ |
| DefaultInfo(runfiles = runfiles), |
| providers[1], |
| ] |
| |
| def _otbn_consttime_test_impl(ctx): |
| """This rule checks if a program or subroutine is constant-time. |
| |
| There are some limitations to this check; see the Python script's |
| documentation for details. In particular, the check may not be able to |
| determine that a program runs in constant-time when in fact it does. |
| However, if the check passes, the program should always run in constant |
| time; that is, the check can produce false negatives but never false |
| positives. |
| |
| This rule expects one dependency of an otbn_binary or otbn_sim_test type, |
| which should provide exactly one `.elf` file. |
| """ |
| |
| # Extract the output .elf file from the output group. |
| elf = [f for t in ctx.attr.deps for f in t[OutputGroupInfo].elf.to_list()] |
| if len(elf) != 1: |
| fail("Expected only one .elf file in dependencies, got: " + str(elf)) |
| elf = elf[0] |
| |
| # Write a very simple script that runs the checker. |
| script_content = "{} {} --verbose".format(ctx.executable._checker.short_path, elf.short_path) |
| if ctx.attr.subroutine: |
| script_content += " --subroutine {}".format(ctx.attr.subroutine) |
| if ctx.attr.secrets: |
| script_content += " --secrets {}".format(" ".join(ctx.attr.secrets)) |
| if ctx.attr.initial_constants: |
| script_content += " --constants {}".format(" ".join(ctx.attr.initial_constants)) |
| ctx.actions.write( |
| output = ctx.outputs.executable, |
| content = script_content, |
| ) |
| |
| # The .elf file must be added to runfiles in order to be visible to the |
| # test at runtime. In addition, we need to add all the runfiles from the |
| # checker script itself (e.g. the Python runtime and dependencies). |
| runfiles = ctx.runfiles(files = [elf]) |
| runfiles = runfiles.merge(ctx.attr._checker[DefaultInfo].default_runfiles) |
| return [DefaultInfo(runfiles = runfiles)] |
| |
| def _otbn_insn_count_range(ctx): |
| """This rule gets min/max possible instruction counts for an OTBN program. |
| """ |
| |
| # Extract the .elf file to check from the dependency list. |
| elf = [f for t in ctx.attr.deps for f in t[OutputGroupInfo].elf.to_list()] |
| if len(elf) != 1: |
| fail("Expected only one .elf file in dependencies, got: " + str(elf)) |
| elf = elf[0] |
| |
| # Command to run the counter script and extract the min/max values. |
| out = ctx.actions.declare_file(ctx.attr.name + ".txt") |
| ctx.actions.run_shell( |
| outputs = [out], |
| inputs = [ctx.file._counter, elf], |
| command = "{} {} > {}".format(ctx.file._counter.path, elf.path, out.path), |
| ) |
| |
| runfiles = ctx.runfiles(files = ([out])) |
| return [DefaultInfo(files = depset([out]), runfiles = runfiles)] |
| |
| otbn_library = rv_rule( |
| implementation = _otbn_library, |
| attrs = { |
| "srcs": attr.label_list(allow_files = True), |
| "_cc_toolchain": attr.label( |
| default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), |
| ), |
| "_otbn_as": attr.label( |
| default = "//hw/ip/otbn/util:otbn_as", |
| executable = True, |
| cfg = "exec", |
| ), |
| }, |
| fragments = ["cpp"], |
| toolchains = ["@rules_cc//cc:toolchain_type"], |
| incompatible_use_toolchain_transition = True, |
| ) |
| |
| otbn_binary = rv_rule( |
| implementation = _otbn_binary, |
| attrs = { |
| "srcs": attr.label_list(allow_files = True), |
| "deps": attr.label_list(providers = [DefaultInfo]), |
| "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")), |
| "_otbn_as": attr.label( |
| default = "//hw/ip/otbn/util:otbn_as", |
| executable = True, |
| cfg = "exec", |
| ), |
| "_otbn_data": attr.label( |
| default = "//hw/ip/otbn/data:all_files", |
| allow_files = True, |
| ), |
| "_wrapper": attr.label( |
| default = "//util:otbn_build", |
| executable = True, |
| cfg = "exec", |
| ), |
| }, |
| fragments = ["cpp"], |
| toolchains = ["@rules_cc//cc:toolchain_type"], |
| incompatible_use_toolchain_transition = True, |
| ) |
| |
| otbn_sim_test = rv_rule( |
| implementation = _otbn_sim_test, |
| test = True, |
| attrs = { |
| "srcs": attr.label_list(allow_files = True), |
| "deps": attr.label_list(providers = [DefaultInfo]), |
| "exp": attr.label(allow_single_file = True), |
| "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")), |
| "_otbn_as": attr.label( |
| default = "//hw/ip/otbn/util:otbn_as", |
| executable = True, |
| cfg = "exec", |
| ), |
| "_otbn_data": attr.label( |
| default = "//hw/ip/otbn/data:all_files", |
| allow_files = True, |
| ), |
| "_simulator": attr.label( |
| default = "//hw/ip/otbn/dv/otbnsim:standalone", |
| executable = True, |
| cfg = "exec", |
| ), |
| "_sim_test_wrapper": attr.label( |
| default = "//hw/ip/otbn/util:otbn_sim_test", |
| executable = True, |
| cfg = "exec", |
| ), |
| "_wrapper": attr.label( |
| default = "//util:otbn_build", |
| executable = True, |
| cfg = "exec", |
| ), |
| }, |
| fragments = ["cpp"], |
| toolchains = ["@rules_cc//cc:toolchain_type"], |
| incompatible_use_toolchain_transition = True, |
| ) |
| |
| otbn_consttime_test = rule( |
| implementation = _otbn_consttime_test_impl, |
| test = True, |
| attrs = { |
| "srcs": attr.label_list(allow_files = True), |
| "deps": attr.label_list(providers = [OutputGroupInfo]), |
| "subroutine": attr.string(), |
| "secrets": attr.string_list(), |
| "initial_constants": attr.string_list(), |
| "_checker": attr.label( |
| default = "//hw/ip/otbn/util:check_const_time", |
| executable = True, |
| cfg = "exec", |
| ), |
| }, |
| ) |
| |
| otbn_insn_count_range = rule( |
| implementation = _otbn_insn_count_range, |
| attrs = { |
| "deps": attr.label_list(providers = [OutputGroupInfo]), |
| "_counter": attr.label( |
| default = "//hw/ip/otbn/util:get_instruction_count_range.py", |
| allow_single_file = True, |
| ), |
| }, |
| ) |