fix: address whole-branch review findings for veer fpga target

- target/veer/BUILD.bazel: add the crate_features select to the
  console rust_library that entry/config already had, so
  #[cfg(feature = "fpga")] in console.rs actually compiles in when
  building for the fpga target_type. Previously the fpga console
  backend (writing to the FPGA wrapper's debug FIFO) was dead code
  and fpga images silently kept using the emulator-only UART address.
- target/veer/tooling/caliptra_runner.py: in the fpga branch of
  load_and_run, drop the Detokenizer/detokenize_text call (this
  target's log backend is log_backend_basic, a plain-text logger, so
  detokenization was a no-op that only added a hard, unmet ELF-file
  runfiles dependency causing a FileNotFoundError at runtime) and scan
  proc.stdout directly instead. Also: print the captured device output
  so operators get diagnostics on both PASS and FAIL, add a timeout to
  the remote subprocess.run call with a TimeoutExpired handler (the
  VeeR core spins forever after signaling its exit sentinel, so an
  unreachable/stuck board no longer hangs indefinitely), and check
  proc.returncode so an outright ssh/remote failure is logged
  distinctly from "board ran but printed nothing".
- .bazelrc: forward VCK190_FPGA_HOST into the k_veer test sandbox
  (Bazel scrubs the test environment by default), matching the
  existing k_ast1060_evb pattern.
diff --git a/.bazelrc b/.bazelrc
index ca05246..88f76ba 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -108,6 +108,8 @@
 # Platform setup for building //target/veer/* artifacts.
 common:k_veer --platforms=//target/veer:veer
 build:k_veer --build_tag_filters=-do_not_build,-kernel_doc_test
+# Usage: VCK190_FPGA_HOST=<host> bazel test --config=k_veer //target/veer/unittest_runner:fpga_test
+test:k_veer --test_env=VCK190_FPGA_HOST
 
 # Site-local overrides. Kept last so developer machine-specific settings (e.g. a
 # local QEMU source override via --override_repository) take precedence. This
diff --git a/target/veer/BUILD.bazel b/target/veer/BUILD.bazel
index 7a5e8f8..d453b2c 100644
--- a/target/veer/BUILD.bazel
+++ b/target/veer/BUILD.bazel
@@ -105,6 +105,12 @@
 rust_library(
     name = "console",
     srcs = ["console.rs"],
+    crate_features = select({
+        ":emulator": ["emulator"],
+        ":fpga": ["fpga"],
+        ":silicon": ["silicon"],
+        "//conditions:default": [],
+    }),
     crate_name = "console_backend",
     edition = "2024",
     target_compatible_with = TARGET_COMPATIBLE_WITH,
diff --git a/target/veer/tooling/caliptra_runner.py b/target/veer/tooling/caliptra_runner.py
index ffeb1c3..e932578 100755
--- a/target/veer/tooling/caliptra_runner.py
+++ b/target/veer/tooling/caliptra_runner.py
@@ -22,6 +22,12 @@
 # VCK190 FPGA board used by the "fpga" interface.
 FPGA_HOST = "VCK190_FPGA_HOST"
 
+# Timeout (seconds) for the remote fpga run. The VeeR core's exit()
+# implementation writes the PASS/FAIL sentinel and then spins forever (there's
+# no way for it to fully halt itself back to the host), so this bounds how
+# long we wait on a stuck or unreachable board rather than hanging forever.
+_FPGA_RUN_TIMEOUT_SECONDS = 300
+
 
 def scan_output_for_result(lines):
     """Scan detokenized output lines for a PASS/FAIL sentinel.
@@ -202,16 +208,42 @@
             remote_bin,
         ]
         _LOG.info("Invoking fpga runner: %s", cmd)
-        proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
+        try:
+            proc = subprocess.run(
+                cmd,
+                capture_output=True,
+                text=True,
+                check=False,
+                timeout=_FPGA_RUN_TIMEOUT_SECONDS,
+            )
+        except subprocess.TimeoutExpired as e:
+            _LOG.fatal(
+                "fpga runner timed out after %s seconds; stdout so far: %s; "
+                "stderr so far: %s",
+                _FPGA_RUN_TIMEOUT_SECONDS,
+                e.stdout,
+                e.stderr,
+            )
+            sys.exit(1)
+
         if proc.stderr:
             _LOG.info("fpga runner stderr: %s", proc.stderr)
 
-        # Reuse the same Detokenizer mechanism the emulator's tokenized
-        # console path uses (see _detokenizer() above), rather than
-        # introducing a second detokenization code path.
-        detokenizer = detokenize.Detokenizer(elf)
-        text = detokenizer.detokenize_text(proc.stdout)
-        result = scan_output_for_result(text.splitlines())
+        if proc.returncode != 0:
+            _LOG.fatal(
+                "ssh/remote command failed with exit code %d: %s",
+                proc.returncode,
+                proc.stderr,
+            )
+            sys.exit(1)
+
+        # This target's kernel config pins its log backend to
+        # log_backend_basic (a plain-text logger; see target/veer/BUILD.bazel's
+        # platform rule), so the board's console output is never tokenized and
+        # there is nothing to detokenize here, unlike the emulator's tokenized
+        # console path (see _detokenizer() above).
+        print(proc.stdout)
+        result = scan_output_for_result(proc.stdout.splitlines())
         if result is None:
             _LOG.fatal(
                 "Device produced no PASS/FAIL sentinel; fpga runner stderr: %s",