Bazel rules for Xilinx Vivado FPGA synthesis, placement, routing, and bitstream generation.
rules_vivado provides Bazel rules to build FPGA designs using Xilinx Vivado. HDL sources flow in through rules_verilog (VerilogInfo) and rules_vhdl (VhdlInfo); the same libraries can be reused for simulation and synthesis.
Add the following to your MODULE.bazel:
bazel_dep(name = "rules_verilog", version = "1.1.1")
bazel_dep(name = "rules_vhdl", version = "0.1.1")
bazel_dep(name = "rules_vivado", version = "0.2.0")
Then register at least one vivado_toolchain so every vivado_* rule can resolve the Xilinx environment script automatically — see Toolchains below.
rules_vivado resolves the Xilinx environment via Bazel toolchain resolution. You declare a vivado_toolchain that wraps a shell script sourcing your Vivado install, wrap it in toolchain(...), and register it from MODULE.bazel.
Write a shell script that sources your Vivado install (and the license server, if applicable). The path convention is tools/vivado/xilinx_env.sh but anywhere works:
#!/usr/bin/env bash
set -e
export HOME=/tmp
source /opt/Xilinx/Vivado/2024.2/settings64.sh
export XILINXD_LICENSE_FILE=2100@license.example.com
Declare a vivado_toolchain and a toolchain() wrapper in BUILD:
load("@rules_vivado//vivado:toolchain.bzl", "vivado_toolchain")
vivado_toolchain(
name = "vivado_local",
xilinx_env = "xilinx_env.sh",
)
toolchain(
name = "vivado_toolchain",
toolchain = ":vivado_local",
toolchain_type = "@rules_vivado//vivado:toolchain_type",
)
Register it from MODULE.bazel:
register_toolchains("//tools/vivado:vivado_toolchain")
That's it — every vivado_* rule now resolves this toolchain automatically; the per-rule xilinx_env attribute (deprecated) is no longer needed.
When you register multiple vivado_toolchain instances (one per Vivado version, one per docker image, …), gate each toolchain() on a constraint_value so Bazel picks the right one. rules_vivado exposes one constraint_value per known release in //vivado/constraints/BUILD.bazel (auto-generated from //vivado/private:versions.bzl VIVADO_VERSIONS).
load("@rules_vivado//vivado:toolchain.bzl", "vivado_toolchain")
vivado_toolchain(
name = "vivado_2024_2",
xilinx_env = "xilinx_env_2024_2.sh",
)
toolchain(
name = "vivado_toolchain_2024_2",
exec_compatible_with = ["@rules_vivado//vivado/constraints/version:2024.2"],
toolchain = ":vivado_2024_2",
toolchain_type = "@rules_vivado//vivado:toolchain_type",
)
platform(
name = "vivado_2024_2_platform",
constraint_values = ["@rules_vivado//vivado/constraints/version:2024.2"],
exec_properties = {
"container-image": "docker://your.registry/vivado:2024.2",
},
parents = ["@platforms//host"],
)
Register both from MODULE.bazel:
register_toolchains("//tools/vivado:vivado_toolchain_2024_2")
register_execution_platforms("//tools/vivado:vivado_2024_2_platform")
The first registered exec platform is the default. Switch versions per build with --platforms=//tools/vivado:vivado_2024_2_platform — that also lets target_compatible_with = ["@rules_vivado//vivado/constraints/version:2024.2"] on a target evaluate against the right constraint (incompatible-at-default, compatible-when-platform-pinned). For per-target switching without a global flag, use a wrapper rule with cfg = transition(...) — see tests/transition.bzl for a with_vivado_version example that takes a list of targets and pins the version for the whole group.
Constraints are the only mechanism — there is no parallel build-setting / flag-driven path. This avoids the two-sources-of-truth problem and keeps the per-version metadata (constraints, exec_properties like container-image) all on the platform object where it belongs.
The same guidance lives in the //vivado:toolchain.bzl module docstring for reference next to the rule itself.
verilog_libraryDefine Verilog/SystemVerilog modules using rules_verilog:
load("@rules_verilog//verilog:defs.bzl", "verilog_library")
verilog_library(
name = "my_design",
srcs = ["my_design.sv"],
data = ["constraints.xdc"],
)
vhdl_libraryDefine VHDL modules using rules_vhdl:
load("@rules_vhdl//vhdl:defs.bzl", "vhdl_library")
vhdl_library(
name = "my_design",
srcs = ["my_design.vhd"],
library = "work",
standard = "2008",
)
vivado_synthesizeSynthesize a design (Verilog or VHDL):
load("@rules_vivado//vivado:defs.bzl", "vivado_synthesize")
vivado_synthesize(
name = "my_design_synth",
module = ":my_design",
module_top = "my_design",
part_number = "xczu28dr-ffvg1517-2-e",
)
vivado_flowRun the complete FPGA flow (synthesis, optimization, placement, routing, bitstream):
load("@rules_vivado//vivado:defs.bzl", "vivado_flow")
vivado_flow(
name = "my_design_bitstream",
module = ":my_design",
module_top = "my_design",
part_number = "xczu28dr-ffvg1517-2-e",
)
This creates intermediate targets:
my_design_bitstream_synth - Synthesismy_design_bitstream_synth_opt - Synthesis optimizationmy_design_bitstream_placement - Placementmy_design_bitstream_place_opt - Placement optimizationmy_design_bitstream_route - Routingmy_design_bitstream - Final bitstream (.bit file)vivado_create_projectCreate a Vivado project without running synthesis:
load("@rules_vivado//vivado:defs.bzl", "vivado_create_project")
vivado_create_project(
name = "my_project",
module = ":my_design",
module_top = "my_design",
part_number = "xczu28dr-ffvg1517-2-e",
)
vivado_create_ipPackage a module as a Vivado IP core:
load("@rules_vivado//vivado:defs.bzl", "vivado_create_ip")
vivado_create_ip(
name = "my_ip",
module = ":my_design",
module_top = "my_design",
part_number = "xczu28dr-ffvg1517-2-e",
ip_vendor = "my_company",
ip_library = "my_lib",
ip_version = "1.0",
)
xsim_testRun simulation tests using Vivado's XSim:
load("@rules_vivado//vivado:defs.bzl", "xsim_test")
xsim_test(
name = "my_design_xsim_test",
module = ":my_testbench",
module_top = "my_testbench",
part_number = "xczu28dr-ffvg1517-2-e",
)
vivado_interface_definitionGenerate IP-XACT interface definition files (bus definition and abstraction definition XML) for custom SystemVerilog interfaces. This allows Vivado to recognize custom interfaces in block designs, enabling visual connection of IP blocks through typed interface ports.
The rule parses a SystemVerilog interface file to extract signal names, directions (from modport declarations), widths, and qualifiers (from SPIRIT: comments). It then generates the corresponding IP-XACT XML files and a TCL setup script.
load("@rules_vivado//vivado:defs.bzl", "vivado_interface_definition")
vivado_interface_definition(
name = "my_interface_def",
src = "my_interface.sv",
interface_name = "my_interface",
vendor = "mycompany.com",
library = "interface",
version = "1.0",
)
The SystemVerilog interface file uses SPIRIT: comments to annotate signals with IP-XACT qualifiers:
interface my_interface #(
parameter int DATA_WIDTH = 32,
parameter int ADDR_WIDTH = 16
);
// SPIRIT:ISADDRESS,REQUIRED
logic [ADDR_WIDTH-1:0] addr;
// SPIRIT:ISDATA,REQUIRED
logic [DATA_WIDTH-1:0] data;
// SPIRIT:REQUIRED
logic valid;
// SPIRIT:REQUIRED
logic ready;
// SPIRIT:OPTIONAL
logic [15:0] status;
modport master(output addr, data, valid, input ready, status);
modport slave(input addr, data, valid, output ready, status);
endinterface
Available SPIRIT: qualifiers:
ISADDRESS - Signal carries address informationISDATA - Signal carries dataISCLOCK - Signal is a clockISRESET - Signal is a resetREQUIRED - Signal must be connectedOPTIONAL - Signal may be left unconnectedThis generates:
my_interface.xml - Bus definition XMLmy_interface_rtl.xml - Abstraction definition XMLmy_interface_if_setup.tcl - TCL setup file for IP packagingTo use the interface definition with other rules, attach it as data on a verilog_library:
verilog_library(
name = "my_interface",
srcs = ["my_interface.sv"],
data = [":my_interface_def"],
)
vivado_create_interface_ipPackage a vivado_interface_definition as an IP catalog entry so Vivado can use the custom interface in block designs:
load("@rules_vivado//vivado:defs.bzl", "vivado_create_interface_ip")
vivado_create_interface_ip(
name = "my_interface_ip",
interface = ":my_interface_def",
module = ":my_interface",
part_number = "xczu28dr-ffvg1517-2-e",
)
IP blocks that use the custom interface should list the interface IP in their ip_blocks dependency:
vivado_create_ip(
name = "my_module_ip",
ip_blocks = [":my_interface_ip"],
module = ":my_module",
module_top = "my_module",
...
)
See tests/ for a complete example using custom BRAM read/write interfaces with a splitter module and block design project.
xilinx_env attributeEach vivado-running rule still accepts a per-target xilinx_env attribute pointing at a shell script. This attribute is deprecated — if it is set, the rule prints a warning at analysis time directing you to register a vivado_toolchain instead. Migrate by dropping the attribute and registering a toolchain as described in Toolchains.
rules_verilogVerilogInfo - Verilog/SystemVerilog library information (sources, headers, data, transitive deps).rules_vhdlVhdlInfo - VHDL library information (sources, library name, standard, transitive deps).rules_vivadoVivadoToolchainInfo - Resolved Xilinx environment.VivadoSynthCheckpointInfo - Synthesis checkpoint (.dcp)VivadoPlacementCheckpointInfo - Placement checkpointVivadoRoutingCheckpointInfo - Routing checkpointVivadoIPBlockInfo - IP block informationVivadoInterfaceInfo - IP-XACT interface definition filesApache License 2.0