blob: 84c07b8b29056bda6d02cd507e3ae23e82997a5b [file] [log] [blame]
# 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,
),
},
)