Separate cocotb build from test for Verilator

- Create a verilator_cocotb_model rule -- this Verilates and compiles in
  the same configuration as cocotb normally does, and provides the
  simulation binary as an output.
- Update verilator_cocotb_test to accept a model parameter instead of a
  verilog target -- this should be the aforementioned
  verilator_cocotb_model rule, and as a bonus this setup allows re-use
  of the simulation binaries between test cases.
- Patch rules_hdl's cocotb_wrapper.py to not actually invoke the build
  -- we just override the path to the simulator binary, and run tests.
- Due to the simulator binary running from a different location, the
  working directory changed and caused issues with the paths of test
  binaries. Use the Bazel runfiles library to find our programs, this
  should be less prone to breakage.

Change-Id: I4f7fdf3501ac4cb4fac6bf8dc6dfe46375e63526
diff --git a/external/0006-Separate-build-from-test-for-Verilator.patch b/external/0006-Separate-build-from-test-for-Verilator.patch
new file mode 100644
index 0000000..cfc36c4
--- /dev/null
+++ b/external/0006-Separate-build-from-test-for-Verilator.patch
@@ -0,0 +1,93 @@
+From 30c00282d60839706f72e21aa5903f6443286af2 Mon Sep 17 00:00:00 2001
+From: Alex Van Damme <atv@google.com>
+Date: Thu, 10 Jul 2025 16:03:33 -0700
+Subject: [PATCH 6/6] Separate build from test for Verilator
+
+---
+ cocotb/BUILD.bazel       |  2 +-
+ cocotb/cocotb.bzl        |  9 ++++++++-
+ cocotb/cocotb_wrapper.py | 16 +++++++++++++++-
+ 3 files changed, 24 insertions(+), 3 deletions(-)
+
+diff --git a/cocotb/BUILD.bazel b/cocotb/BUILD.bazel
+index ed8a3a0..72b19fb 100644
+--- a/cocotb/BUILD.bazel
++++ b/cocotb/BUILD.bazel
+@@ -25,5 +25,5 @@ py_binary(
+     python_version = "PY3",
+     srcs_version = "PY3",
+     visibility = ["//visibility:public"],
+-    deps = [],
++    deps = ["@bazel_tools//tools/python/runfiles"],
+ )
+diff --git a/cocotb/cocotb.bzl b/cocotb/cocotb.bzl
+index 2b0a3c2..90c70bc 100644
+--- a/cocotb/cocotb.bzl
++++ b/cocotb/cocotb.bzl
+@@ -161,6 +161,7 @@ def _get_test_command(ctx, verilog_files, vhdl_files):
+         waves_args +
+         seed_args +
+         test_module_args +
++        (" --model {}".format(ctx.executable.model.short_path) if ctx.attr.sim_name == "verilator" else "" ) +
+         ("&& cp -fr `pwd`/sim_build/simv.vdb $TEST_UNDECLARED_OUTPUTS_DIR" if ctx.attr.sim_name == "vcs" else "")
+     )
+ 
+@@ -185,7 +186,8 @@ def _cocotb_test_impl(ctx):
+         files = ctx.files.cocotb_wrapper +
+                 verilog_files +
+                 vhdl_files +
+-                ctx.files.test_module,
++                ctx.files.test_module +
++                ctx.files.model,
+         transitive_files = _collect_transitive_files(ctx),
+     ).merge(
+         _collect_transitive_runfiles(ctx),
+@@ -253,6 +255,11 @@ _cocotb_test_attrs = {
+         doc = "Verilog include directories",
+         default = [],
+     ),
++    "model": attr.label(
++        executable = True,
++        doc = "Verilated model binary",
++        cfg = "exec",
++    ),
+     "parameters": attr.string_dict(
+         doc = "Verilog parameters or VHDL generics",
+         default = {},
+diff --git a/cocotb/cocotb_wrapper.py b/cocotb/cocotb_wrapper.py
+index ce392a9..fce1b6e 100644
+--- a/cocotb/cocotb_wrapper.py
++++ b/cocotb/cocotb_wrapper.py
+@@ -165,6 +165,11 @@ def cocotb_argument_parser():
+         default="results.xml",
+         help="Name of xUnit XML file to store test results in",
+     )
++    parser.add_argument(
++        "--model",
++        default=None,
++        help="Verilated model binary",
++    )
+ 
+     return parser
+ 
+@@ -188,7 +193,16 @@ if __name__ == "__main__":
+ 
+     cocotb_tools.runner.MAX_PARALLEL_BUILD_JOBS = 32
+     runner = get_runner(args.sim)
+-    runner.build(**build_flags)
++
++    if args.sim == "verilator":
++        import os
++        from bazel_tools.tools.python.runfiles import runfiles
++        r = runfiles.Create()
++        sim_build = os.path.dirname(r.Rlocation(f"kelvin_hw/{args.model}"))
++        test_flags['build_dir'] = sim_build
++        test_flags['extra_env']['LD_LIBRARY_PATH'] = r.Rlocation("kelvin_hw/external/kelvin_pip_deps_cocotb/cocotb/libs")
++    else:
++        runner.build(**build_flags)
+     results_xml = runner.test(**test_flags)
+     (num_tests, num_failed) = get_results(results_xml)
+     sys.exit(num_failed)
+-- 
+2.50.0.727.gbf7dc18ff4-goog
+
diff --git a/kelvin_test_utils/BUILD b/kelvin_test_utils/BUILD
index b07303a..a358b45 100644
--- a/kelvin_test_utils/BUILD
+++ b/kelvin_test_utils/BUILD
@@ -48,6 +48,7 @@
     deps = [
         requirement("cocotb"),
         requirement("pyocd"),
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = [
         "//toolchain:gdb",
diff --git a/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py b/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
index 8bea479..cad8813 100644
--- a/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
+++ b/kelvin_test_utils/core_mini_axi_pyocd_gdbserver.py
@@ -361,7 +361,7 @@
                     cmdfile.write(f'{cmd}\n')
                 cmdfile.flush()
                 args = [
-                    '../toolchain/gdb',
+                    '../../../toolchain/gdb',
                     '-x',
                     cmdfile.name,
                     elf.name,
diff --git a/rules/coco_tb.bzl b/rules/coco_tb.bzl
index 0be028a..7a7ee0e 100644
--- a/rules/coco_tb.bzl
+++ b/rules/coco_tb.bzl
@@ -14,12 +14,110 @@
 
 """Convinence wrapper for Verilator driven cocotb."""
 
-load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
 load("@kelvin_hw//third_party/python:requirements.bzl", "requirement")
 load("@rules_hdl//cocotb:cocotb.bzl", "cocotb_test")
 load("@rules_python//python:defs.bzl", "py_library")
 
+def _verilator_cocotb_model_impl(ctx):
+    hdl_toplevel = ctx.attr.hdl_toplevel
+    outdir_name = hdl_toplevel + "_build"
+
+    output_file = ctx.actions.declare_file(outdir_name + "/" + hdl_toplevel)
+    make_log = ctx.actions.declare_file(outdir_name + "/make.log")
+    outdir = output_file.dirname
+
+    verilator_root = "$PWD/{}.runfiles/kelvin_hw/external/verilator".format(ctx.executable._verilator_bin.path)
+    cocotb_lib_path = "$PWD/{}".format(ctx.files._cocotb_verilator_lib[0].dirname)
+    verilator_cmd = " ".join("""
+        VERILATOR_ROOT={verilator_root} {verilator} \
+            -cc \
+            --exe \
+            -Mdir {outdir} \
+            --top-module {hdl_toplevel} \
+            --vpi \
+            --public-flat-rw \
+            --prefix Vtop \
+            -o {hdl_toplevel} \
+            -LDFLAGS "-Wl,-rpath external/kelvin_pip_deps_cocotb/cocotb/lib -L{cocotb_lib_path} -lcocotbvpi_verilator" \
+            {cflags} \
+            $PWD/{verilator_cpp} \
+            {verilog_source}
+    """.strip().split("\n")).format(
+        verilator = ctx.executable._verilator_bin.path,
+        verilator_root = verilator_root,
+        outdir = outdir,
+        hdl_toplevel = hdl_toplevel,
+        cocotb_lib_path = cocotb_lib_path,
+        cflags = " ".join(ctx.attr.cflags),
+        verilator_cpp = ctx.files._cocotb_verilator_cpp[0].path,
+        verilog_source = ctx.file.verilog_source.path,
+    )
+
+    make_cmd = "PATH=`dirname $(which ld)`:$PATH make -j $(nproc) -C {outdir} -f Vtop.mk CXX=`which g++` AR=`which ar` LINK=`which g++` > {make_log} 2>&1".format(
+        outdir=outdir,
+        cocotb_lib_path=cocotb_lib_path,
+        make_log=make_log.path,
+    )
+
+    script = " && ".join([verilator_cmd.strip(), make_cmd])
+
+    ctx.actions.run_shell(
+        outputs = [output_file, make_log],
+        tools = ctx.files._verilator_bin,
+        inputs = depset(
+            transitive = [
+                depset(ctx.files._verilator),
+                depset(ctx.files._cocotb_verilator_lib),
+                depset(ctx.files._cocotb_verilator_cpp),
+                depset([ctx.file.verilog_source]),
+            ],
+        ),
+        command = script,
+        mnemonic = "Verilate",
+    )
+
+    return [
+        DefaultInfo(
+            files = depset([output_file, make_log]),
+            runfiles = ctx.runfiles(files=[output_file, make_log]),
+            executable = output_file,
+        ),
+        OutputGroupInfo(
+            all_files = depset([output_file, make_log]),
+        ),
+    ]
+
+verilator_cocotb_model = rule(
+    implementation = _verilator_cocotb_model_impl,
+    attrs = {
+        "verilog_source": attr.label(allow_single_file = True, mandatory = True),
+        "hdl_toplevel": attr.string(mandatory = True),
+        "cflags": attr.string_list(default = []),
+        "_verilator": attr.label(
+            default = "@verilator//:verilator",
+            executable = True,
+            cfg = "exec",
+        ),
+        "_verilator_bin": attr.label(
+            default = "@verilator//:verilator_bin",
+            executable = True,
+            cfg = "exec",
+        ),
+        "_cocotb_verilator_lib": attr.label(
+            default = "@kelvin_pip_deps_cocotb//:verilator_libs",
+            allow_files = True,
+        ),
+        "_cocotb_verilator_cpp": attr.label(
+            default = "@kelvin_pip_deps_cocotb//:verilator_srcs",
+            allow_files = True,
+        ),
+    },
+    executable = True,
+    toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
+)
+
 def verilator_cocotb_test(name,
+                          model,
                           hdl_toplevel,
                           test_module,
                           deps=[],
@@ -31,7 +129,8 @@
         sim = [
             "@verilator//:verilator",
             "@verilator//:verilator_bin",
-        ])
+        ],
+    )
 
     # Wrap in py_library so we can forward data
     py_library(
@@ -47,6 +146,7 @@
 
     cocotb_test(
         name = name,
+        model = model,
         hdl_toplevel = hdl_toplevel,
         test_module = test_module,
         deps = [
diff --git a/rules/repos.bzl b/rules/repos.bzl
index 1a56692..a11e079 100644
--- a/rules/repos.bzl
+++ b/rules/repos.bzl
@@ -40,7 +40,8 @@
             "@kelvin_hw//external:0002-Update-cocotb-script-to-support-newer-version.patch",
             "@kelvin_hw//external:0003-Export-vdb-via-undeclared-test-outputs.patch",
             "@kelvin_hw//external:0004-More-jobs-for-cocotb.patch",
-            "@kelvin_hw//external:0005-Use-num_failed-for-exit-code.patch"
+            "@kelvin_hw//external:0005-Use-num_failed-for-exit-code.patch",
+            "@kelvin_hw//external:0006-Separate-build-from-test-for-Verilator.patch",
         ],
         patch_args = ["-p1"],
     )
diff --git a/tests/cocotb/BUILD b/tests/cocotb/BUILD
index 45f2ab4..1f6dc48 100644
--- a/tests/cocotb/BUILD
+++ b/tests/cocotb/BUILD
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-load("//rules:coco_tb.bzl", "vcs_cocotb_test", "verilator_cocotb_test")
+load("//rules:coco_tb.bzl", "vcs_cocotb_test", "verilator_cocotb_test", "verilator_cocotb_model")
 load("//rules:kelvin_v2.bzl", "kelvin_v2_binary")
 load("//rules:utils.bzl", "template_rule")
 load("@kelvin_hw//third_party/python:requirements.bzl", "requirement")
@@ -42,37 +42,52 @@
     "-Wno-WIDTHEXPAND",
     "-Wno-WIDTHTRUNC",
     "-Wno-UNSIGNED",
+    "-DUSE_GENERIC=\"\"",
 ]
 
+verilator_cocotb_model(
+    name = "core_mini_axi_model",
+    hdl_toplevel = "CoreMiniAxi",
+    verilog_source = "//hdl/chisel/src/kelvin:CoreMiniAxi.sv",
+    cflags = VERILATOR_BUILD_ARGS,
+)
+
+verilator_cocotb_model(
+    name = "core_mini_debug_axi_model",
+    hdl_toplevel = "CoreMiniDebugAxi",
+    verilog_source = "//hdl/chisel/src/kelvin:CoreMiniDebugAxi.sv",
+    cflags = VERILATOR_BUILD_ARGS,
+)
+
+verilator_cocotb_model(
+    name = "rvv_core_mini_axi_model",
+    hdl_toplevel = "RvvCoreMiniAxi",
+    verilog_source = "//hdl/chisel/src/kelvin:RvvCoreMiniAxi.sv",
+    cflags = VERILATOR_BUILD_ARGS,
+)
+
 template_rule(
     verilator_cocotb_test,
     {
         "core_mini_axi_sim_cocotb": {
             "hdl_toplevel": "CoreMiniAxi",
-            "verilog_sources": [
-                "//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"
-            ],
+            "model": ":core_mini_axi_model",
             "size": "enormous",
         },
         "rvv_core_mini_axi_sim_cocotb": {
             "hdl_toplevel": "RvvCoreMiniAxi",
-            "verilog_sources": [
-                "//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"
-            ],
+            "model": ":rvv_core_mini_axi_model",
             "size": "enormous",
             "tags": ["manual"], # This suite takes a really long time
         },
     },
-    build_args = VERILATOR_BUILD_ARGS,
-    defines = {
-        "USE_GENERIC" : "",
-    },
     waves = True,
     seed = "42",
     test_module = ["core_mini_axi_sim.py"],
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
         requirement("tqdm"),
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = COCOTB_TEST_BINARY_TARGETS,
 )
@@ -80,26 +95,7 @@
 verilator_cocotb_test(
     name = "core_mini_axi_debug_cocotb",
     hdl_toplevel = "CoreMiniDebugAxi",
-    verilog_sources = [
-                "//hdl/chisel/src/kelvin:core_mini_axi_debug_cc_library_verilog"
-    ],
-    build_args = [
-        "-Wno-WIDTH",
-        "-Wno-CASEINCOMPLETE",
-        "-Wno-LATCH",
-        "-Wno-SIDEEFFECT",
-        "-Wno-MULTIDRIVEN",
-        "-Wno-UNOPTFLAT",
-        "-Wno-CASEOVERLAP",
-        # Warnings that we disable for fpnew
-        "-Wno-ASCRANGE",
-        "-Wno-WIDTHEXPAND",
-        "-Wno-WIDTHTRUNC",
-        "-Wno-UNSIGNED",
-    ],
-    defines = {
-        "USE_GENERIC" : "",
-    },
+    model = ":core_mini_debug_axi_model",
     waves = True,
     seed = "42",
     test_module = ["core_mini_axi_debug.py"],
@@ -107,6 +103,7 @@
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
         "//kelvin_test_utils:core_mini_axi_pyocd_gdbserver",
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = [
         ":fptr.elf",
@@ -175,6 +172,7 @@
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
         requirement("tqdm"),
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = COCOTB_TEST_BINARY_TARGETS + [
         ":coverage_exclude.cfg",
@@ -193,19 +191,14 @@
 verilator_cocotb_test(
     name = "rvv_assembly_cocotb_test",
     hdl_toplevel = "RvvCoreMiniAxi",
-    verilog_sources = [
-        "//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"
-    ],
-    build_args = VERILATOR_BUILD_ARGS,
-    defines = {
-        "USE_GENERIC" : "",
-    },
+    model = ":rvv_core_mini_axi_model",
     waves = True,
     seed = "42",
     test_module = ["rvv_assembly_cocotb_test.py"],
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
         requirement("tqdm"),
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = RVV_TEST_BINARY_TARGETS,
     size = "large",
@@ -301,18 +294,13 @@
 verilator_cocotb_test(
     name = "rvv_load_store_test",
     hdl_toplevel = "RvvCoreMiniAxi",
-    verilog_sources = [
-        "//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"
-    ],
-    build_args = VERILATOR_BUILD_ARGS,
-    defines = {
-        "USE_GENERIC" : "",
-    },
+    model = ":rvv_core_mini_axi_model",
     waves = True,
     seed = "42",
     test_module = ["rvv_load_store_test.py"],
     deps = [
         "//kelvin_test_utils:sim_test_fixture",
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = [
         '//tests/cocotb/rvv/load_store:load8_indexed_m1.elf',
@@ -343,18 +331,13 @@
 verilator_cocotb_test(
     name = "rvv_arithmetic_cocotb_test",
     hdl_toplevel = "RvvCoreMiniAxi",
-    verilog_sources = [
-        "//hdl/chisel/src/kelvin:rvv_core_mini_axi_cc_library_verilog"
-    ],
-    build_args = VERILATOR_BUILD_ARGS,
-    defines = {
-        "USE_GENERIC" : "",
-    },
+    model = ":rvv_core_mini_axi_model",
     waves = True,
     seed = "42",
     test_module = ["rvv_arithmetic_cocotb_test.py"],
     deps = [
         "//kelvin_test_utils:sim_test_fixture",
+        "@bazel_tools//tools/python/runfiles",
         requirement("tqdm"),
     ],
     data = RVV_TEST_BINARY_TARGETS,
diff --git a/tests/cocotb/core_mini_axi_debug.py b/tests/cocotb/core_mini_axi_debug.py
index 80bdff5..5439dc4 100644
--- a/tests/cocotb/core_mini_axi_debug.py
+++ b/tests/cocotb/core_mini_axi_debug.py
@@ -18,6 +18,7 @@
 from cocotb.triggers import ClockCycles
 from kelvin_test_utils.core_mini_axi_interface import CoreMiniAxiInterface, DmCmdType, DmRspOp
 from kelvin_test_utils.core_mini_axi_pyocd_gdbserver import CoreMiniAxiGDBServer
+from bazel_tools.tools.python.runfiles import runfiles
 
 @cocotb.test()
 async def core_mini_axi_debug_gdbserver(dut):
@@ -27,9 +28,10 @@
     cocotb.start_soon(core_mini_axi.clock.start())
 
     gdbserver = CoreMiniAxiGDBServer(core_mini_axi)
+    r = runfiles.Create()
 
     # Just poke some FPU register.
-    with open("../tests/cocotb/registers.elf", "rb") as f:
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/registers.elf"), "rb") as f:
         cmds = [
             "info reg f0",
         ]
@@ -37,7 +39,7 @@
 
     # Test which calls memcpy through a function pointer.
     # Ensure we correctly break in memcpy.
-    with open("../tests/cocotb/fptr.elf", "rb") as f:
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/fptr.elf"), "rb") as f:
         memcpy = core_mini_axi.lookup_symbol(f, "memcpy")
         cmds = [
             f"break *{hex(memcpy)}",
@@ -50,7 +52,7 @@
 
     # Test which calls a computation function repeatedly.
     # Check the result of the second iteration, which should be 5.
-    with open("../tests/cocotb/math.elf", "rb") as f:
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/math.elf"), "rb") as f:
         cmds = [
             f"break math",
             "continue",
@@ -170,7 +172,8 @@
     rsp = await core_mini_axi.dm_write(0x10, dmcontrol)
     assert rsp["op"] == DmRspOp.SUCCESS
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.execute_from(entry_point)
         wait_for_halted_asserted = False
@@ -191,7 +194,8 @@
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
 
         await core_mini_axi.dm_request_halt()
@@ -239,7 +243,8 @@
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.dm_request_halt()
 
@@ -282,7 +287,8 @@
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.dm_request_halt()
         await core_mini_axi.execute_from(entry_point)
@@ -298,7 +304,8 @@
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.dm_request_halt()
 
@@ -349,7 +356,8 @@
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
 
-    with open("../tests/cocotb/noop.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/noop.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.dm_request_halt()
 
@@ -422,7 +430,8 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
-    with open("../tests/cocotb/registers.elf", "rb") as f:
+    r = runfiles.Create()
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/registers.elf"), "rb") as f:
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.execute_from(entry_point)
         await core_mini_axi.wait_for_wfi()
diff --git a/tests/cocotb/core_mini_axi_sim.py b/tests/cocotb/core_mini_axi_sim.py
index 7bec163..a4b9f26 100644
--- a/tests/cocotb/core_mini_axi_sim.py
+++ b/tests/cocotb/core_mini_axi_sim.py
@@ -20,6 +20,7 @@
 import random
 
 from kelvin_test_utils.core_mini_axi_interface import AxiBurst, AxiResp,CoreMiniAxiInterface
+from bazel_tools.tools.python.runfiles import runfiles
 
 
 @cocotb.test()
@@ -72,9 +73,10 @@
     core_mini_axi = CoreMiniAxiInterface(dut)
     cocotb.start_soon(core_mini_axi.clock.start())
     await core_mini_axi.init()
+    r = runfiles.Create()
 
     for slot in range(0,4):
-      with open(f"../tests/cocotb/wfi_slot_{slot}.elf", "rb") as f:
+      with open(r.Rlocation(f"kelvin_hw/tests/cocotb/wfi_slot_{slot}.elf"), "rb") as f:
         await core_mini_axi.reset()
         entry_point = await core_mini_axi.load_elf(f)
         await core_mini_axi.execute_from(entry_point)
@@ -107,8 +109,9 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
+    r = runfiles.Create()
 
-    with open(f"../tests/cocotb/stress_test.elf", "rb") as f:
+    with open(r.Rlocation("kelvin_hw/tests/cocotb/stress_test.elf"), "rb") as f:
       halt = core_mini_axi.lookup_symbol(f, "halt")
       dtcm_vec = core_mini_axi.lookup_symbol(f, "dtcm_vec")
       entry_point = await core_mini_axi.load_elf(f)
@@ -147,8 +150,9 @@
   await core_mini_axi.init()
   await core_mini_axi.reset()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  with open("../tests/cocotb/align_test.elf", "rb") as f:
+  with open(r.Rlocation("kelvin_hw/tests/cocotb/align_test.elf"), "rb") as f:
     entry_point = await core_mini_axi.load_elf(f)
     await core_mini_axi.execute_from(entry_point)
 
@@ -161,8 +165,9 @@
   await core_mini_axi.init()
   await core_mini_axi.reset()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  with open("../tests/cocotb/finish_txn_before_halt.elf", "rb") as f:
+  with open(r.Rlocation("kelvin_hw/tests/cocotb/finish_txn_before_halt.elf"), "rb") as f:
     entry_point = await core_mini_axi.load_elf(f)
     await core_mini_axi.execute_from(entry_point)
     await core_mini_axi.wait_for_halted()
@@ -178,8 +183,10 @@
   core_mini_axi = CoreMiniAxiInterface(dut)
   await core_mini_axi.init()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  riscv_test_elfs = glob.glob("../tests/cocotb/riscv-tests/*.elf")
+  riscv_test_path = r.Rlocation("kelvin_hw/tests/cocotb/riscv-tests")
+  riscv_test_elfs = [os.path.join(riscv_test_path, f) for f in os.listdir(riscv_test_path) if f.endswith(".elf")]
   for elf in tqdm.tqdm(riscv_test_elfs):
     with open(elf, "rb") as f:
       await core_mini_axi.reset()
@@ -193,8 +200,10 @@
   core_mini_axi = CoreMiniAxiInterface(dut)
   await core_mini_axi.init()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  riscv_dv_elfs = glob.glob("../tests/cocotb/riscv-dv/*.o")
+  riscv_dv_path = r.Rlocation("kelvin_hw/tests/cocotb/riscv-dv")
+  riscv_dv_elfs = [os.path.join(riscv_dv_path, f) for f in os.listdir(riscv_dv_path) if f.endswith(".o")]
   with tqdm.tqdm(riscv_dv_elfs) as t:
     for elf in tqdm.tqdm(riscv_dv_elfs):
       t.set_postfix({"binary": os.path.basename(elf)})
@@ -250,8 +259,10 @@
   await core_mini_axi.init()
   await core_mini_axi.reset()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  exceptions_elfs = glob.glob("../tests/cocotb/exceptions/*.elf")
+  exceptions_path = r.Rlocation("kelvin_hw/tests/cocotb/exceptions")
+  exceptions_elfs = [os.path.join(exceptions_path, f) for f in os.listdir(exceptions_path) if f.endswith(".elf")]
   with tqdm.tqdm(exceptions_elfs) as t:
     for elf in tqdm.tqdm(exceptions_elfs):
       t.set_postfix({"binary": os.path.basename(elf)})
@@ -268,8 +279,10 @@
   await core_mini_axi.init()
   await core_mini_axi.reset()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  kelvin_isa_elfs = glob.glob("../tests/cocotb/kelvin_isa/*.elf")
+  kelvin_isa_path = r.Rlocation("kelvin_hw/tests/cocotb/kelvin_isa")
+  kelvin_isa_elfs = [os.path.join(kelvin_isa_path, f) for f in os.listdir(kelvin_isa_path) if f.endswith(".elf")]
   for elf in tqdm.tqdm(kelvin_isa_elfs):
     with open(elf, "rb") as f:
       await core_mini_axi.reset()
@@ -340,8 +353,9 @@
   await core_mini_axi.init()
   await core_mini_axi.reset()
   cocotb.start_soon(core_mini_axi.clock.start())
+  r = runfiles.Create()
 
-  with open("../tests/cocotb/float_csr_interlock_test.elf", "rb") as f:
+  with open(r.Rlocation("kelvin_hw/tests/cocotb/float_csr_interlock_test.elf"), "rb") as f:
     entry_point = await core_mini_axi.load_elf(f)
     await core_mini_axi.execute_from(entry_point)
 
diff --git a/tests/cocotb/rvv_arithmetic_cocotb_test.py b/tests/cocotb/rvv_arithmetic_cocotb_test.py
index 520d8cb..7bc5f90 100644
--- a/tests/cocotb/rvv_arithmetic_cocotb_test.py
+++ b/tests/cocotb/rvv_arithmetic_cocotb_test.py
@@ -17,6 +17,7 @@
 import re
 import numpy as np
 
+from bazel_tools.tools.python.runfiles import runfiles
 from kelvin_test_utils.sim_test_fixture import Fixture
 
 
@@ -58,11 +59,13 @@
     pattern_extract = re.compile("rvv_(.*)_(.*)_m1.elf")
 
 
+    r = runfiles.Create()
     fixture = await Fixture.Create(dut)
     with tqdm.tqdm(m1_vanilla_op_elfs) as t:
         for elf_name in tqdm.tqdm(m1_vanilla_op_elfs):
+            elf_path = r.Rlocation("kelvin_hw/tests/cocotb/rvv/arithmetics/" + elf_name)
             await fixture.load_elf_and_lookup_symbols(
-                '../tests/cocotb/rvv/arithmetics/' + elf_name,
+                elf_path,
                 ['in_buf_1', 'in_buf_2', 'out_buf'],
             )
             math_op, dtype = pattern_extract.match(elf_name).groups()
diff --git a/tests/cocotb/rvv_assembly_cocotb_test.py b/tests/cocotb/rvv_assembly_cocotb_test.py
index f8b0154..e436fca 100644
--- a/tests/cocotb/rvv_assembly_cocotb_test.py
+++ b/tests/cocotb/rvv_assembly_cocotb_test.py
@@ -2,6 +2,7 @@
 import numpy as np
 import argparse
 from kelvin_test_utils.core_mini_axi_interface import CoreMiniAxiInterface
+from bazel_tools.tools.python.runfiles import runfiles
 
 @cocotb.test()
 async def core_mini_rvv_load(dut):
@@ -15,8 +16,9 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
+    r = runfiles.Create()
 
-    elf_path = "../tests/cocotb/rvv/rvv_load.elf"
+    elf_path = r.Rlocation("kelvin_hw/tests/cocotb/rvv/rvv_load.elf")
     num_test_bytes = 16
     intial_pass = True
     if not elf_path:
@@ -62,8 +64,9 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
+    r = runfiles.Create()
 
-    elf_path = "../tests/cocotb/rvv/rvv_add.elf"
+    elf_path = r.Rlocation("kelvin_hw/tests/cocotb/rvv/rvv_add.elf")
     num_test_bytes = 16
     intial_pass = True
 
@@ -111,8 +114,9 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
+    r = runfiles.Create()
 
-    elf_path = "../tests/cocotb/rvv/vstart_store.elf"
+    elf_path = r.Rlocation("kelvin_hw/tests/cocotb/rvv/vstart_store.elf")
     if not elf_path:
         raise ValueError("elf_path must consist a valid path")
     with open(elf_path, "rb") as f:
diff --git a/tests/cocotb/rvv_load_store_test.py b/tests/cocotb/rvv_load_store_test.py
index 7fadb4a..5fcdd5d 100644
--- a/tests/cocotb/rvv_load_store_test.py
+++ b/tests/cocotb/rvv_load_store_test.py
@@ -16,6 +16,7 @@
 import numpy as np
 
 from kelvin_test_utils.sim_test_fixture import Fixture
+from bazel_tools.tools.python.runfiles import runfiles
 
 
 async def vector_load_store(
@@ -31,8 +32,9 @@
     Each test performs some kind of patterned copy from `in_buf` to `out_buf`.
     """
     fixture = await Fixture.Create(dut)
+    r = runfiles.Create()
     await fixture.load_elf_and_lookup_symbols(
-        '../tests/cocotb/rvv/load_store/' + elf_name,
+        r.Rlocation('kelvin_hw/tests/cocotb/rvv/load_store/' + elf_name),
         ['in_buf', 'out_buf'],
     )
 
@@ -69,8 +71,9 @@
     Each test performs a gather operation and writes the result to an output.
     """
     fixture = await Fixture.Create(dut)
+    r = runfiles.Create()
     await fixture.load_elf_and_lookup_symbols(
-        '../tests/cocotb/rvv/load_store/' + elf_name,
+        r.Rlocation('kelvin_hw/tests/cocotb/rvv/load_store/' + elf_name),
         ['input_indices', 'input_data', 'output_data'],
     )
 
@@ -108,8 +111,9 @@
     Each test loads indices and data and performs a scatter operation.
     """
     fixture = await Fixture.Create(dut)
+    r = runfiles.Create()
     await fixture.load_elf_and_lookup_symbols(
-        '../tests/cocotb/rvv/load_store/' + elf_name,
+        r.Rlocation('kelvin_hw/tests/cocotb/rvv/load_store/' + elf_name),
         ['input_indices', 'input_data', 'output_data'],
     )
 
diff --git a/tests/cocotb/tutorial/BUILD b/tests/cocotb/tutorial/BUILD
index a661c3b..504b9f6 100644
--- a/tests/cocotb/tutorial/BUILD
+++ b/tests/cocotb/tutorial/BUILD
@@ -17,68 +17,31 @@
 
 verilator_cocotb_test(
     name = "tutorial",
-    defines = {
-        "USE_GENERIC" : "",
-    },
     waves = True,
     hdl_toplevel = "CoreMiniAxi",
     seed = "42",
     test_module = ["tutorial.py"],
-    verilog_sources = [
-        "//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"
-    ],
+    model = "//tests/cocotb:core_mini_axi_model",
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
     ],
-    build_args = [
-        "-Wno-WIDTH",
-        "-Wno-CASEINCOMPLETE",
-        "-Wno-LATCH",
-        "-Wno-SIDEEFFECT",
-        "-Wno-MULTIDRIVEN",
-        "-Wno-CASEOVERLAP",
-        "-Wno-UNOPTFLAT",
-        "-Wno-ASCRANGE",
-        "-Wno-WIDTHEXPAND",
-        "-Wno-WIDTHTRUNC",
-        "-Wno-UNSIGNED",
-    ],
     data = glob(["**/*.elf"]),
 )
 
-
 verilator_cocotb_test(
     name = "hello_world_float_core_mini_axi",
-    defines = {
-        "USE_GENERIC" : "",
-    },
     waves = True,
     hdl_toplevel = "CoreMiniAxi",
     seed = "42",
     test_module = ["hello_world_float_core_mini_axi.py"],
-    verilog_sources = [
-        "//hdl/chisel/src/kelvin:core_mini_axi_cc_library_verilog"
-    ],
+    model = "//tests/cocotb:core_mini_axi_model",
     deps = [
         "//kelvin_test_utils:core_mini_axi_sim_interface",
-    ],
-    build_args = [
-        "-Wno-WIDTH",
-        "-Wno-CASEINCOMPLETE",
-        "-Wno-LATCH",
-        "-Wno-SIDEEFFECT",
-        "-Wno-MULTIDRIVEN",
-        "-Wno-CASEOVERLAP",
-        "-Wno-UNOPTFLAT",
-        "-Wno-ASCRANGE",
-        "-Wno-WIDTHEXPAND",
-        "-Wno-WIDTHTRUNC",
-        "-Wno-UNSIGNED",
+        "@bazel_tools//tools/python/runfiles",
     ],
     data = ["//examples:kelvin_v2_hello_world_add_floats.elf"],
 )
 
-
 kelvin_v2_binary(
     name="kelvin_v2_program",
     srcs = ["program.cc"],
diff --git a/tests/cocotb/tutorial/hello_world_float_core_mini_axi.py b/tests/cocotb/tutorial/hello_world_float_core_mini_axi.py
index 3df762e..e103c35 100644
--- a/tests/cocotb/tutorial/hello_world_float_core_mini_axi.py
+++ b/tests/cocotb/tutorial/hello_world_float_core_mini_axi.py
@@ -16,6 +16,7 @@
 import numpy as np
 import argparse
 from kelvin_test_utils.core_mini_axi_interface import CoreMiniAxiInterface
+from bazel_tools.tools.python.runfiles import runfiles
 
 @cocotb.test()
 async def core_mini_axi_tutorial(dut):
@@ -25,9 +26,10 @@
     await core_mini_axi.init()
     await core_mini_axi.reset()
     cocotb.start_soon(core_mini_axi.clock.start())
+    r = runfiles.Create()
 
     #Elf file is generated from bazel build //examples:kelvin_v2_hello_world_add_floats
-    elf_path = "../examples/kelvin_v2_hello_world_add_floats.elf"
+    elf_path = r.Rlocation("kelvin_hw/examples/kelvin_v2_hello_world_add_floats.elf")
     if not elf_path:
       raise ValueError("elf_path must consist a valid path ")
     #Load your program into ITCM with "load_elf"
diff --git a/third_party/python/requirements.bzl b/third_party/python/requirements.bzl
index e52de31..2fedda3 100644
--- a/third_party/python/requirements.bzl
+++ b/third_party/python/requirements.bzl
@@ -31,6 +31,14 @@
     deps = {deps},
     tags = ["pypi_name={pypi_name}","pypi_version={pypi_version}"],
 )
+filegroup(
+    name = "verilator_srcs",
+    srcs = glob(["cocotb/share/lib/verilator/*.cpp"]),
+)
+filegroup(
+    name = "verilator_libs",
+    srcs = glob(["cocotb/libs/*.so"]),
+)
 """.format(pypi_name = pypi_name, pypi_version = pypi_version, deps = deps)
 
 def install_deps():