function(get_yosys_cells_sim arch cells_sim)
    # This simple function returns a specific cells library
    # file from the yosys share dir, corresponding to the
    # desired device family
    #
    # Arguments:
    #   - arch      : device family. E.g. xilinx, ecp5, etc.
    #   - cells_sim : variable name where to store the cells_sim library path
    execute_process(
        COMMAND
            yosys-config --datdir
        OUTPUT_VARIABLE
            prefix
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )

    set(${cells_sim} "${prefix}/${arch}/cells_sim.v" PARENT_SCOPE)
endfunction()

function(add_xc7_test)
    # ~~~
    # add_xc7_test(
    #    name <name>
    #    board_list <board_list>
    #    sources <sources list>
    #    [absolute_sources <sources list>]
    #    [tcl <tcl>]
    #    [top <top name>]
    #    [techmap <techmap file>]
    #    [testbench]
    #    [disable_vivado_test]
    # )
    #
    # Generates targets to run desired tests
    #
    # Arguments:
    #   - name: test name. This must be unique and no other tests with the same
    #           name should exist
    #   - board_list: list of boards, one for each test
    #   - tcl: tcl script used for synthesis
    #   - sources: list of HDL sources
    #   - absoulute_sources (optional): list of sources with an absoulute path.
    #   - top (optional): name of the top level module.
    #                     If not provided, "top" is assigned as top level module
    #   - techmap (optional): techmap file used during synthesis
    #   - testbench (optional): verilog testbench to verify the correctness of the design
    #                           generated by fasm2bels
    #   - disable_vivado_test (optional): disables the Vivado bitstream generation to run DRC checks
    #
    # Targets generated:
    #   - xc7-<name>-<board>-json     : synthesis output
    #   - xc7-<name>-<board>-netlist  : logical interchange netlist
    #   - xc7-<name>-<board>-phys     : physical interchange netlist
    #   - xc7-<name>-<board>-fasm     : fasm file
    #   - xc7-<name>-<board>-bit      : bitstream
    #   - xc7-<name>-<board>-dcp      : DCP

    set(options disable_vivado_test)
    set(oneValueArgs name tcl top techmap testbench)
    set(multiValueArgs board_list sources absolute_sources)

    cmake_parse_arguments(
        add_xc7_test
        "${options}"
        "${oneValueArgs}"
        "${multiValueArgs}"
        ${ARGN}
    )

    set(name ${add_xc7_test_name})
    set(top ${add_xc7_test_top})
    set(testbench ${add_xc7_test_testbench})
    set(techmap ${add_xc7_test_techmap})
    set(tcl ${add_xc7_test_tcl})
    set(disable_vivado_test ${add_xc7_test_disable_vivado_test})

    set(sources)
    foreach(source ${add_xc7_test_sources})
        list(APPEND sources ${CMAKE_CURRENT_SOURCE_DIR}/${source})
    endforeach()
    foreach(source ${add_xc7_test_absolute_sources})
        list(APPEND sources ${source})
    endforeach()

    if (DEFINED techmap)
        set(techmap ${CMAKE_CURRENT_SOURCE_DIR}/${add_xc7_test_techmap})
    endif()

    if (NOT DEFINED top)
        # Setting default top value
        set(top "top")
    endif()

    set(synth_tcl "${CMAKE_SOURCE_DIR}/tests/common/synth.tcl")
    if (DEFINED tcl)
        set(synth_tcl ${CMAKE_CURRENT_SOURCE_DIR}/${add_xc7_test_tcl})
    endif()

    set(quiet_cmd ${CMAKE_SOURCE_DIR}/utils/quiet_cmd.sh)

    get_target_property(YOSYS programs YOSYS)
    get_target_property(NEXTPNR_INTERCHANGE programs NEXTPNR_INTERCHANGE)
    get_target_property(XCFASM programs XCFASM)
    get_target_property(PYTHON3 programs PYTHON3)

    foreach(board ${add_xc7_test_board_list})
        # Get board properties
        get_property(device_family TARGET board-${board} PROPERTY DEVICE_FAMILY)
        get_property(device TARGET board-${board} PROPERTY DEVICE)
        get_property(package TARGET board-${board} PROPERTY PACKAGE)
        get_property(part TARGET board-${board} PROPERTY PART)

        set(test_name "${name}-${board}")
        add_custom_target(xc7-${test_name}-common-synth-tcl DEPENDS ${synth_tcl})
        set(xdc ${CMAKE_CURRENT_SOURCE_DIR}/${board}.xdc)
        set(device_loc ${NEXTPNR_SHARE_DIR}/devices/${device}.device)
        set(chipdb_loc ${NEXTPNR_SHARE_DIR}/chipdb/${device}.bin)

        set(output_dir ${CMAKE_CURRENT_BINARY_DIR}/${board})
        add_custom_command(
            OUTPUT ${output_dir}
            COMMAND ${CMAKE_COMMAND} -E make_directory ${output_dir}
        )

        add_custom_target(xc7-${test_name}-output-dir DEPENDS ${output_dir})

        # Synthesis
        set(synth_json ${output_dir}/${name}.json)
        set(synth_log ${output_dir}/${name}.synth.log)
        set(synth_verilog ${output_dir}/${name}.synth.v)
        add_custom_command(
            OUTPUT ${synth_json}
            COMMAND ${CMAKE_COMMAND} -E env
                SOURCES="${sources}"
                OUT_JSON=${synth_json}
                OUT_VERILOG=${synth_verilog}
                TECHMAP=${techmap}
                ${quiet_cmd}
                ${YOSYS} -c ${synth_tcl} -l ${synth_log}
            DEPENDS
                ${sources}
                ${techmap}
                ${synth_tcl}
                xc7-${test_name}-output-dir
        )

        add_custom_target(xc7-${test_name}-json DEPENDS ${synth_json})

        if(DEFINED testbench)
            add_simulation_test(
                name ${name}
                board ${board}
                sources ${sources}
                deps xc7-${test_name}-output-dir
                testbench ${testbench}
                simlib_dir ${XILINX_UNISIM_DIR}
                extra_libs ${XILINX_UNISIM_DIR}/../glbl.v
            )

            add_simulation_test(
                name post-synth-${name}
                board ${board}
                sources ${synth_verilog}
                deps xc7-${test_name}-json
                testbench ${testbench}
                simlib_dir ${XILINX_UNISIM_DIR}
                extra_libs ${XILINX_UNISIM_DIR}/../glbl.v
            )
        endif()

        # Logical netlist
        set(netlist ${output_dir}/${name}.netlist)
        add_custom_command(
            OUTPUT ${netlist}
            COMMAND ${CMAKE_COMMAND} -E env
                ${quiet_cmd}
                python3 -mfpga_interchange.yosys_json
                    --schema_dir ${INTERCHANGE_SCHEMA_PATH}
                    --device ${device_loc}
                    --top ${top}
                    ${synth_json}
                    ${netlist}
            DEPENDS
                xc7-${test_name}-json
                ${device_loc}
                ${synth_json}
        )

        add_custom_target(xc7-${test_name}-netlist DEPENDS ${netlist})

        # Physical netlist
        set(phys ${output_dir}/${name}.phys)
        set(phys_log ${output_dir}/${name}.phys.log)
        add_custom_command(
            OUTPUT ${phys}
            COMMAND
                ${quiet_cmd}
                ${NEXTPNR_INTERCHANGE}
                    --chipdb ${chipdb_loc}
                    --xdc ${xdc}
                    --netlist ${netlist}
                    --phys ${phys}
                    --package ${package}
                    --log ${phys_log}
            DEPENDS
                xc7-${test_name}-netlist
                ${xdc}
                ${chipdb_loc}
                ${netlist}
        )

        # Physical Netlist YAML
        set(phys_yaml ${output_dir}/${name}.phys.yaml)
        add_custom_command(
            OUTPUT ${phys_yaml}
            COMMAND
                ${PYTHON3} -mfpga_interchange.convert
                    --schema_dir ${INTERCHANGE_SCHEMA_PATH}
                    --schema physical
                    --input_format capnp
                    --output_format yaml
                    ${phys}
                    ${phys_yaml}
            DEPENDS
                xc7-${test_name}-phys
                ${phys}
        )

        add_custom_target(xc7-${test_name}-phys DEPENDS ${phys})
        add_custom_target(xc7-${test_name}-phys-yaml DEPENDS ${phys_yaml})

        # DCP generation target
        set(dcp ${output_dir}/${name}.dcp)
        add_custom_command(
            OUTPUT ${dcp}
            COMMAND ${CMAKE_COMMAND} -E env
                RAPIDWRIGHT_PATH=${RAPIDWRIGHT_PATH}
                ${INVOKE_RAPIDWRIGHT}
                com.xilinx.rapidwright.interchange.PhysicalNetlistToDcp
                ${netlist} ${phys} ${xdc} ${dcp}
            DEPENDS
                ${INVOKE_RAPIDWRIGHT}
                xc7-${test_name}-netlist
                xc7-${test_name}-phys
                ${netlist}
                ${phys}
        )

        add_custom_target(xc7-${test_name}-dcp DEPENDS ${dcp})

        # Bitstream generation target from DCP
        set(vivado_tcl ${CMAKE_SOURCE_DIR}/tests/common/vivado.tcl)
        set(run_vivado ${CMAKE_SOURCE_DIR}/utils/run_vivado.sh)
        set(dcp_bit ${output_dir}/${name}.dcp.bit)
        add_custom_command(
            OUTPUT ${dcp_bit}
            COMMAND ${CMAKE_COMMAND} -E env
                VIVADO_SETTINGS=${VIVADO_SETTINGS}
                OUTPUT_DIR=${output_dir}
                DCP_FILE=${dcp}
                BIT_FILE=${dcp_bit}
                ${quiet_cmd}
                ${run_vivado} -mode batch -source ${vivado_tcl}
            DEPENDS
                xc7-${test_name}-dcp
                ${run_vivado}
                ${vivado_tcl}
                ${dcp}
        )

        add_custom_target(xc7-${test_name}-dcp-bit DEPENDS ${dcp_bit})
        if(NOT ${disable_vivado_test})
            add_dependencies(all-xc7-vivado-bit-tests xc7-${test_name}-dcp-bit)
        endif()


        # Output FASM target
        set(fasm ${output_dir}/${name}.fasm)
        add_custom_command(
            OUTPUT ${fasm}
            COMMAND ${CMAKE_COMMAND} -E env
                ${quiet_cmd}
                ${PYTHON3} -mfpga_interchange.fasm_generator
                    --schema_dir ${INTERCHANGE_SCHEMA_PATH}
                    --family xc7
                    ${device_loc}
                    ${netlist}
                    ${phys}
                    ${fasm}
            DEPENDS
                ${device_target}
                xc7-${test_name}-netlist
                xc7-${test_name}-phys
                xc7-${test_name}-dcp
                ${netlist}
                ${phys}
        )

        add_custom_target(xc7-${test_name}-fasm DEPENDS ${fasm})

        # Bitstream generation target
        set(bit ${output_dir}/${name}.bit)
        add_custom_command(
            OUTPUT ${bit}
            COMMAND ${CMAKE_COMMAND} -E env
                ${quiet_cmd}
                ${XCFASM}
                    --db-root ${PRJXRAY_DB_DIR}/${device_family}
                    --part ${part}
                    --part_file ${PRJXRAY_DB_DIR}/${device_family}/${part}/part.yaml
                    --sparse
                    --emit_pudc_b_pullup
                    --fn_in ${fasm}
                    --bit_out ${bit}
            DEPENDS
                xc7-${test_name}-fasm
                ${fasm}
            )

        add_custom_target(xc7-${test_name}-bit DEPENDS ${bit})
        add_dependencies(all-xc7-tests xc7-${test_name}-bit)
    endforeach()

endfunction()

function(add_xc7_validation_test)
    # ~~~
    # add_xc7_validation_test(
    #    name <name>
    #    board_list <board>
    #    [disable_vivado_test]
    #    [testbench]
    # )
    #
    # Generates targets to run desired tests
    #
    # Arguments:
    #   - name: test name. This must be unique and no other tests with the same
    #           name should exist
    #   - board_list: list of boards, one for each test
    #   - disable_vivado_test (optional): disables the Vivado bitstream generation to run DRC checks
    #   - testbench (optional): verilog testbench to verify the correctness of the design
    #                           generated by fasm2bels
    #
    # Targets generated:
    #   - <device>-channels-db              : device database for fasm2bels
    #   - xc7-<name>-<board>-fasm2bels      : target to run fasm2bels
    #   - xc7-<name>-<board>-fasm2bels-dcp  : dcp from the fasm2bels outputs
    #   - xc7-<name>-<board>-tcl            : tcl file to run Vivado
    #   - xc7-<name>-<board>-vivado-bit     : vivado-generated bitstream

    set(options disable_vivado_test)
    set(oneValueArgs name testbench)
    set(multiValueArgs board_list sources)

    cmake_parse_arguments(
        add_xc7_validation_test
        "${options}"
        "${oneValueArgs}"
        "${multiValueArgs}"
        ${ARGN}
    )

    set(name ${add_xc7_validation_test_name})
    set(disable_vivado_test ${add_xc7_validation_test_disable_vivado_test})
    set(testbench ${add_xc7_validation_test_testbench})

    set(quiet_cmd ${CMAKE_SOURCE_DIR}/utils/quiet_cmd.sh)

    get_target_property(PYTHON3 programs PYTHON3)
    get_target_property(YOSYS programs YOSYS)
    get_target_property(BITREAD programs BITREAD)

    foreach(board ${add_xc7_validation_test_board_list})
        get_property(device_family TARGET board-${board} PROPERTY DEVICE_FAMILY)
        get_property(device TARGET board-${board} PROPERTY DEVICE)
        get_property(package TARGET board-${board} PROPERTY PACKAGE)
        get_property(part TARGET board-${board} PROPERTY PART)

        set(test_name "${name}-${board}")

        set(device_channels_dir ${CMAKE_BINARY_DIR}/channels)
        set(device_channels ${device_channels_dir}/${device}.db)
        if(NOT TARGET ${device}-channels-db)
            # Build the channels.db file required by fasm2bels
            add_custom_command(
                OUTPUT ${device_channels}
                COMMAND
                    ${CMAKE_COMMAND} -E make_directory ${device_channels_dir}
                COMMAND
                    ${quiet_cmd}
                    ${PYTHON3} -mfasm2bels.database.create_channels
                        --db-root ${PRJXRAY_DB_DIR}/${device_family}
                        --part ${part}
                        --connection-database ${device_channels}
                )

            add_custom_target(${device}-channels-db DEPENDS ${device_channels})
        endif()

        set(output_dir ${CMAKE_CURRENT_BINARY_DIR}/${board})
        set(bit ${output_dir}/${name}.bit)
        set(bit_target xc7-${test_name}-bit)

        # Run fasm2bels to get logical and physical netlists
        set(netlist ${output_dir}/${name}.fasm2bels.netlist)
        set(phys ${output_dir}/${name}.fasm2bels.phys)
        set(interchange_xdc ${output_dir}/${name}.fasm2bels.inter.xdc)
        set(fasm ${output_dir}/${name}.fasm2bels.fasm)
        set(verilog ${output_dir}/${name}.fasm2bels.v)
        set(xdc ${output_dir}/${name}.fasm2bels.xdc)
        add_custom_command(
            OUTPUT ${netlist} ${phys} ${xdc} ${fasm} ${interchange_xdc} ${verilog}
            COMMAND
                ${quiet_cmd}
                ${YOSYS} -p "read_json ${output_dir}/${name}.json\; write_blif ${output_dir}/${name}.eblif"
            COMMAND
                ${quiet_cmd}
                ${PYTHON3} -mfasm2bels
                    --db_root ${PRJXRAY_DB_DIR}/${device_family}
                    --part ${part}
                    --connection_database ${device_channels}
                    --bitread ${BITREAD}
                    --bit_file ${bit}
                    --fasm_file ${fasm}
                    --eblif ${output_dir}/${name}.eblif
                    --verilog_file ${verilog}
                    --input_xdc ${CMAKE_CURRENT_SOURCE_DIR}/${board}.xdc
                    --xdc_file ${xdc}
                    --logical_netlist ${netlist}
                    --physical_netlist ${phys}
                    --interchange_xdc ${interchange_xdc}
                    --interchange_capnp_schema_dir ${INTERCHANGE_SCHEMA_PATH}
            DEPENDS
                ${bit_target}
                ${device}-channels-db
                ${bit}
        )

        add_custom_target(
            xc7-${test_name}-fasm2bels
            DEPENDS
                ${netlist}
                ${phys}
                ${interchange_xdc}
                ${fasm}
                ${verilog}
                ${xdc}
        )

        # DCP generation target
        set(dcp ${output_dir}/${name}.fasm2bels.dcp)
        add_custom_command(
            OUTPUT ${dcp}
            COMMAND ${CMAKE_COMMAND} -E env
                RAPIDWRIGHT_PATH=${RAPIDWRIGHT_PATH}
                ${quiet_cmd}
                ${INVOKE_RAPIDWRIGHT}
                com.xilinx.rapidwright.interchange.PhysicalNetlistToDcp
                ${netlist} ${phys} ${xdc} ${dcp}
            DEPENDS
                ${INVOKE_RAPIDWRIGHT}
                xc7-${test_name}-fasm2bels
        )

        add_custom_target(xc7-${test_name}-fasm2bels-dcp DEPENDS ${dcp})

        # Generate bitstream from Vivado
        set(tcl ${CMAKE_SOURCE_DIR}/tests/common/vivado.tcl)
        set(run_vivado ${CMAKE_SOURCE_DIR}/utils/run_vivado.sh)
        set(vivado_bit ${output_dir}/${name}.fasm2bels.bit)
        add_custom_command(
            OUTPUT ${vivado_bit}
            COMMAND ${CMAKE_COMMAND} -E env
                VIVADO_SETTINGS=${VIVADO_SETTINGS}
                OUTPUT_DIR=${output_dir}
                DCP_FILE=${dcp}
                BIT_FILE=${vivado_bit}
                ${quiet_cmd}
                ${run_vivado} -mode batch -source ${tcl}
            DEPENDS
                xc7-${test_name}-fasm2bels-dcp
                ${dcp}
                ${run_vivado}
                ${tcl}
        )

        add_custom_target(xc7-${test_name}-vivado-bit DEPENDS ${vivado_bit})

        if(NOT ${disable_vivado_test})
            add_dependencies(all-xc7-vivado-bit-tests xc7-${test_name}-vivado-bit)
        endif()

        add_dependencies(all-xc7-validation-tests xc7-${test_name}-fasm2bels-dcp)

        if(DEFINED testbench)
            add_simulation_test(
                name fasm2bels-${name}
                board ${board}
                sources ${verilog}
                deps xc7-${test_name}-fasm2bels-dcp
                testbench ${testbench}
                simlib_dir ${XILINX_UNISIM_DIR}
                extra_libs ${XILINX_UNISIM_DIR}/../glbl.v
            )
        endif()
    endforeach()
endfunction()

function(add_simulation_test)
    # ~~~
    # add_simulation_test(
    #    name <name>
    #    board <board>
    #    testbench <testbench>
    #    deps
    #    [extra_libs <extra libraries>]
    #    [simlib_dir <simulation lib directory>]
    # )
    #
    # Generates targets to run desired simulation tests.
    #
    # This function should not be called directly, but within another test function.
    #
    # Arguments:
    #   - name: test name. This must be unique and no other tests with the same
    #           name should exist
    #   - board: board name. This is used to get the output directory
    #   - testbench: verilog testbench file that instantiates the DUT and performs
    #                basic tests
    #   - deps: dependencies to be met prior to running the simulation test
    #   - extra_libs (optional): verilog libraires for vendor-specific cells
    #   - simlib_dir (optional): simulation library directory
    #
    # Targets generated:
    #   - sim-test-${test}-${board}-vvp : generates the VVP and VCD files
    #   - sim-test-${test}-${board}     : runs VVP

    set(options)
    set(oneValueArgs board name testbench deps simlib_dir)
    set(multiValueArgs sources extra_libs)

    cmake_parse_arguments(
        add_simulation_test
        "${options}"
        "${oneValueArgs}"
        "${multiValueArgs}"
        ${ARGN}
    )

    set(name ${add_simulation_test_name})
    set(board ${add_simulation_test_board})
    set(sources ${add_simulation_test_sources})
    set(deps ${add_simulation_test_deps})
    set(extra_libs ${add_simulation_test_extra_libs})
    set(simlib_dir ${add_simulation_test_simlib_dir})
    set(testbench ${CMAKE_CURRENT_SOURCE_DIR}/${add_simulation_test_testbench})

    set(test_name "${name}-${board}")

    get_target_property(VVP programs VVP)
    get_target_property(IVERILOG programs IVERILOG)

    set(output_dir ${CMAKE_CURRENT_BINARY_DIR}/${board})

    set(utils_dir ${CMAKE_SOURCE_DIR}/utils)
    set(quiet_cmd ${utils_dir}/quiet_cmd.sh)

    set(simlib_dir_opt "")
    if(DEFINED simlib_dir)
        set(simlib_dir_opt "-y${simlib_dir}")
    endif()

    set(vvp ${name}.vvp)
    set(vvp_path ${output_dir}/${vvp})
    add_custom_command(
        OUTPUT ${vvp_path}
        COMMAND
            ${quiet_cmd}
            ${IVERILOG}
                -v
                -I ${utils_dir}
                -DVCD=${output_dir}/${name}.vcd
                -o ${vvp}
                ${simlib_dir_opt}
                ${extra_libs}
                ${sources}
                ${testbench}
        DEPENDS
            ${IVERILOG}
            ${deps}
            ${sources}
            ${testbench}
        WORKING_DIRECTORY
            ${output_dir}
    )

    add_custom_target(sim-test-${test_name}-vvp DEPENDS ${vvp_path})

    add_custom_target(
        sim-test-${test_name}
        COMMAND
            ${quiet_cmd}
            ${VVP}
                -v
                -N
                ${vvp_path}
        DEPENDS
            ${VVP}
            sim-test-${test_name}-vvp
    )

    add_dependencies(all-simulation-tests sim-test-${test_name})
endfunction()
