feat(build): Add FPGA toolchain support and enhance binary rules

Change-Id: Iede67e519368f5f58ac368cf1d40acc909cd37b0
diff --git a/.bazelrc b/.bazelrc
index 46c1a8c..a78752b 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -31,16 +31,19 @@
 build --action_env=VC_STATIC_HOME
 build --action_env=VERDI_HOME
 build --action_env=LM_LICENSE_FILE
+build --action_env=XILINXD_LICENSE_FILE
 test --build_tag_filters="-vcs,-renode,-verilator,-synthesis"
 test --test_tag_filters="-vcs,-renode,-verilator,-synthesis"
 test --action_env=VCS_HOME
 test --action_env=VC_STATIC_HOME
 test --action_env=VERDI_HOME
 test --action_env=LM_LICENSE_FILE
+test --action_env=XILINXD_LICENSE_FILE
 run --action_env=VCS_HOME
 run --action_env=VC_STATIC_HOME
 run --action_env=VERDI_HOME
 run --action_env=LM_LICENSE_FILE
+run --action_env=XILINXD_LICENSE_FILE
 
 # Add config to enable VCS targets.
 build:vcs --build_tag_filters="vcs"
diff --git a/WORKSPACE b/WORKSPACE
index ec83784..187ab52 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -14,7 +14,7 @@
 
 workspace(name = "kelvin_hw")
 
-load("//rules:repos.bzl", "kelvin_repos", "renode_repos", "cvfpu_repos", "rvvi_repos")
+load("//rules:repos.bzl", "kelvin_repos", "renode_repos", "cvfpu_repos", "rvvi_repos", "fpga_repos")
 
 kelvin_repos()
 
@@ -88,10 +88,47 @@
     python_version = "3.9",
 )
 
+fpga_repos()
+
+load("@lowrisc_opentitan_gh//rules:nonhermetic.bzl", "nonhermetic_repo")
+
+nonhermetic_repo(name = "nonhermetic")
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+pip_parse(
+   name = "ot_python_deps",
+   requirements_lock = "@lowrisc_opentitan_gh//:python-requirements.txt",
+   python_interpreter_target = "@python39_x86_64-unknown-linux-gnu//:python",
+)
+
 load("//third_party/python:requirements.bzl", "install_deps")
 install_deps()
 
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+# OpenTitan's requirements need this, but for some reason do not provide it.
+http_archive(
+    name = "ot_python_deps_importlib_metadata",
+    urls = [
+        "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl",
+    ],
+    sha256 = "e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd",
+    type = "zip",
+    build_file_content = """
+package(default_visibility = ["//visibility:public"])
+py_library(
+    name = "pkg",
+    srcs = glob(["**/*.py"]),
+    data = [] + glob(["**/*"], exclude=["**/* *", "**/*.dist-info/RECORD", "**/*.py", "**/*.pyc"]),
+    imports = ["."],
+    tags = ["pypi_name=importlib_metadata","pypi_version=8.7.0"],
+)
+""",
+)
+
+load("@ot_python_deps//:requirements.bzl", ot_install_deps = "install_deps")
+ot_install_deps()
 
 http_archive(
     name = "toolchain_kelvin_v2",
diff --git a/fpga/0001-Export-hw-ip_templates.patch b/fpga/0001-Export-hw-ip_templates.patch
new file mode 100644
index 0000000..4620e1b
--- /dev/null
+++ b/fpga/0001-Export-hw-ip_templates.patch
@@ -0,0 +1,44 @@
+From 626a4c6e202972b7930797cb7a43f8ee6ac6b991 Mon Sep 17 00:00:00 2001
+From: Alex Van Damme <atv@google.com>
+Date: Fri, 18 Jul 2025 14:48:57 -0700
+Subject: [PATCH] Export hw/ip_templates
+
+---
+ hw/BUILD              | 3 +--
+ hw/ip_templates/BUILD | 2 ++
+ 2 files changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/hw/BUILD b/hw/BUILD
+index 2fb71c7d81..d7e28a91a8 100644
+--- a/hw/BUILD
++++ b/hw/BUILD
+@@ -15,7 +15,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_list_flag")
+ 
+ package(default_visibility = ["//visibility:public"])
+ 
+-exports_files(["tool_requirements.py"])
++exports_files(["tool_requirements.py"] + glob(["vendor/lowrisc_ibex/**"]))
+ 
+ filegroup(
+     name = "doc_files",
+@@ -177,7 +177,6 @@ filegroup(
+         # dv_macros are needed by Ibex, so include this back in.
+         "//hw/dv/sv:dv_macros",
+         "//hw/ip:rtl_files",
+-        "//hw/top_earlgrey:rtl_files",
+     ],
+     visibility = ["//visibility:public"],
+ )
+diff --git a/hw/ip_templates/BUILD b/hw/ip_templates/BUILD
+index 3e8bb41965..a9431cda2e 100644
+--- a/hw/ip_templates/BUILD
++++ b/hw/ip_templates/BUILD
+@@ -1,3 +1,5 @@
+ # Copyright lowRISC contributors (OpenTitan project).
+ # Licensed under the Apache License, Version 2.0, see LICENSE for details.
+ # SPDX-License-Identifier: Apache-2.0
++
++exports_files(glob(["**"]))
+-- 
+2.50.0.727.gbf7dc18ff4-goog
+
diff --git a/fpga/BUILD b/fpga/BUILD
new file mode 100644
index 0000000..36a1e8d
--- /dev/null
+++ b/fpga/BUILD
@@ -0,0 +1,14 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
diff --git a/rules/kelvin_v2.bzl b/rules/kelvin_v2.bzl
index 3a4dc6d..22423da 100644
--- a/rules/kelvin_v2.bzl
+++ b/rules/kelvin_v2.bzl
@@ -87,14 +87,23 @@
         if CcInfo in dep:
             compilation_contexts.append(dep[CcInfo].compilation_context)
             linking_contexts.append(dep[CcInfo].linking_context)
+
+    sources = []
+    headers = []
+    for src in ctx.files.srcs:
+        if src.extension in ["h", "hh", "hpp"]:
+            headers.append(src)
+        else:
+            sources.append(src)
+
     (_compilation_context, compilation_outputs) = cc_common.compile(
         actions = ctx.actions,
         cc_toolchain = cc_toolchain,
         feature_configuration = feature_configuration,
         name = ctx.label.name,
-        srcs = ctx.files.srcs,
+        srcs = sources,
         compilation_contexts = compilation_contexts,
-        private_hdrs = ctx.files.hdrs,
+        private_hdrs = headers + ctx.files.hdrs,
         user_compile_flags = ctx.attr.copts,
         defines = ctx.attr.defines,
     )
@@ -112,13 +121,58 @@
         output_type = "executable",
     )
 
+    out_bin = ctx.actions.declare_file("{}.bin".format(ctx.label.name))
+    objcopy_tool = cc_toolchain.objcopy_executable
+
+    ctx.actions.run(
+        outputs = [out_bin],
+        inputs = [linking_outputs.executable] + cc_toolchain.all_files.to_list(),
+        executable = objcopy_tool,
+        arguments = [
+            "-O",
+            "binary",
+            linking_outputs.executable.path,
+            out_bin.path,
+        ],
+        mnemonic = "ObjCopy",
+    )
+
+    out_vmem = ctx.actions.declare_file("{}.vmem".format(ctx.label.name))
+    word_size = ctx.attr.word_size
+    srec_cat_vmem_args = [
+        out_bin.path,
+        "-binary",
+        "-byte-swap",
+        str(word_size // 8),
+        "-fill",
+        "0xff",
+        "-within",
+        out_bin.path,
+        "-binary",
+        "-range-pad",
+        str(word_size // 8),
+        "-o",
+        out_vmem.path,
+        "-vmem",
+        str(word_size),
+    ]
+    ctx.actions.run(
+        outputs = [out_vmem],
+        inputs = [out_bin],
+        executable = "srec_cat",
+        arguments = srec_cat_vmem_args,
+        mnemonic = "SrecCat",
+    )
+
     return [
         DefaultInfo(
-            files = depset([linking_outputs.executable]),
+            files = depset([linking_outputs.executable, out_bin, out_vmem]),
         ),
         OutputGroupInfo(
-            all_files = depset([linking_outputs.executable]),
+            all_files = depset([linking_outputs.executable, out_bin, out_vmem]),
             elf_file = depset([linking_outputs.executable]),
+            bin_file = depset([out_bin]),
+            vmem_file = depset([out_vmem]),
         ),
     ]
 
@@ -134,6 +188,7 @@
         "linker_script": attr.label(allow_single_file = True),
         "linker_script_includes": attr.label_list(default = [], allow_files = True),
         "semihosting": attr.bool(),
+        "word_size": attr.int(default = 32),
         "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
     },
     fragments = ["cpp"],
@@ -145,6 +200,8 @@
         srcs,
         tags = [],
         semihosting = False,
+        linker_script = "@kelvin_hw//toolchain:kelvin_tcm.ld",
+        word_size = 32,
         **kwargs):
     """A helper macro for generating binary artifacts for the Kelvin V2 core.
 
@@ -156,6 +213,7 @@
       srcs: The c source files.
       tags: build tags.
       semihosting: Enable htif-style semihosting
+      linker_script: Linker script to construct the final binary.
       **kwargs: Additional arguments forward to cc_binary.
     Emits rules:
       filegroup              named: <name>.bin
@@ -171,10 +229,11 @@
     _kelvin_v2_binary(
         name = name,
         srcs = srcs,
-        linker_script = "@kelvin_hw//toolchain:kelvin_tcm.ld",
+        linker_script = linker_script,
         semihosting = semihosting,
         tags = tags,
         deps = deps,
+        word_size = word_size,
         **kwargs
     )
 
@@ -183,4 +242,18 @@
         srcs = [name],
         output_group = "elf_file",
         tags = tags,
+    )
+
+    native.filegroup(
+        name = "{}.vmem".format(name),
+        srcs = [name],
+        output_group = "vmem_file",
+        tags = tags,
+    )
+
+    native.filegroup(
+        name = "{}.bin".format(name),
+        srcs = [name],
+        output_group = "bin_file",
+        tags = tags,
     )
\ No newline at end of file
diff --git a/rules/repos.bzl b/rules/repos.bzl
index 64cb1c0..4b18ee6 100644
--- a/rules/repos.bzl
+++ b/rules/repos.bzl
@@ -160,3 +160,15 @@
         ],
         patch_args = ["-p1"],
     )
+
+def fpga_repos():
+    http_archive(
+        name = "lowrisc_opentitan_gh",
+        urls = ["https://github.com/lowRISC/opentitan/archive/1b1945fd76799666156f817e163222725c518c59.zip"],
+        sha256 = "b881378cdffee2284a88c2032c9fb13e68c889f1cac38cf715b0cff7b40fcf7e",
+        strip_prefix = "opentitan-1b1945fd76799666156f817e163222725c518c59",
+        patches = [
+            "@kelvin_hw//fpga:0001-Export-hw-ip_templates.patch",
+        ],
+        patch_args = ["-p1"],
+    )
\ No newline at end of file
diff --git a/rules/utils.bzl b/rules/utils.bzl
index a877032..be8ec88 100644
--- a/rules/utils.bzl
+++ b/rules/utils.bzl
@@ -44,4 +44,34 @@
     rule(
         name = rule_name,
         **rule_kwargs
-    )
\ No newline at end of file
+    )
+
+def cc_embed_data(
+        name,
+        srcs,
+        var_name = "data",
+        testonly = False,
+        **kwargs):
+    """A rule to merge binary files into a C include file.
+    Args:
+        name: The name of the target.
+        srcs: The input binary source files.
+        var_name: The variable name of the array in output C include file.
+        testonly: The target is built for test only.
+        **kwargs: Extra arguments forwards to genrule.
+    """
+    xxd_cmd = """
+        xxd -i $< > $@;
+        sed -i -e 's#\\w*_len#{}_len#g' $@;
+        sed -i -e "s#\\w*\\[\\]#{}\\[\\]#g" $@;
+        sed -i -e 's/unsigned/const unsigned/g' $@
+        """.format(var_name, var_name)
+    outs = ["{}.{}".format(name, "h")]
+    native.genrule(
+        name = name,
+        srcs = srcs,
+        outs = outs,
+        cmd = xxd_cmd,
+        testonly = testonly,
+        **kwargs
+    )
diff --git a/third_party/python/BUILD b/third_party/python/BUILD
index 0a2669d..a4c83a0 100644
--- a/third_party/python/BUILD
+++ b/third_party/python/BUILD
@@ -11,3 +11,5 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
+package(default_visibility = ["//visibility:public"])
diff --git a/third_party/python/requirements.bzl b/third_party/python/requirements.bzl
index 2fedda3..b5fed94 100644
--- a/third_party/python/requirements.bzl
+++ b/third_party/python/requirements.bzl
@@ -364,3 +364,40 @@
             ],
         ),
     )
+
+    # FPGA
+    http_archive(
+        name = "kelvin_pip_deps_mako",
+        urls = [
+            "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl",
+        ],
+        sha256 = "baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59",
+        type = "zip",
+        build_file_content = _build_file_content(pypi_name = "mako", pypi_version = "1.3.10"),
+    )
+
+    http_archive(
+        name = "kelvin_pip_deps_hjson",
+        urls = [
+            "https://files.pythonhosted.org/packages/1f/7f/13cd798d180af4bf4c0ceddeefba2b864a63c71645abc0308b768d67bb81/hjson-3.1.0-py3-none-any.whl",
+        ],
+        sha256 = "65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89",
+        type = "zip",
+        build_file_content = _build_file_content(pypi_name = "hjson", pypi_version = "3.1.0"),
+    )
+
+    http_archive(
+        name = "kelvin_pip_deps_fusesoc",
+        urls = [
+            "https://files.pythonhosted.org/packages/cb/a8/10f62458dda2ca07c49477448e643b10f30825b7741fdb6ec3e6188b6999/fusesoc-2.4.3-py3-none-any.whl",
+        ],
+        sha256 = "9ab4a82a5b7d4decbeb8f76049673a1b0806732ab8f807fee285bbc0452b3dc3",
+        type = "zip",
+        build_file_content = _build_file_content(
+            pypi_name = "fusesoc",
+            pypi_version = "2.4.3",
+            deps = [
+                "@kelvin_pip_deps_hjson//:pkg",
+            ],
+        ),
+    )
diff --git a/toolchain/cc_toolchain_config.bzl b/toolchain/cc_toolchain_config.bzl
index 44a5d1b..b86d290 100644
--- a/toolchain/cc_toolchain_config.bzl
+++ b/toolchain/cc_toolchain_config.bzl
@@ -76,6 +76,10 @@
             path = "/bin/false",
         ),
         tool_path(
+            name = "objcopy",
+            path = "wrappers/objcopy",
+        ),
+        tool_path(
             name = "strip",
             path = "/bin/false",
         ),
diff --git a/toolchain/wrappers/objcpy b/toolchain/wrappers/objcpy
deleted file mode 120000
index da2bdd9..0000000
--- a/toolchain/wrappers/objcpy
+++ /dev/null
@@ -1 +0,0 @@
-driver.sh
\ No newline at end of file