CMake Integration

FetchContent Example

Under the docs/cmake folder of the Nunavut repo is an example project that uses CMake’s FetchContent module to integrate nnvg code generation into a CMake project.

CMakeLists Figure

This example CMakeLists.txt builds the upstream example binary using only cmake, git, and python. It demonstrates both how to integrate with nnvg and how to run Nunavut from source which avoids managing Python environments for your build.

#
# Copyright (C) OpenCyphal Development Team  <opencyphal.org>
# Copyright Amazon.com Inc. or its affiliates.
# SPDX-License-Identifier: MIT
#

cmake_minimum_required(VERSION 3.27.0 FATAL_ERROR)

project("Nunavut Cmake Example"
        VERSION 1.0
        LANGUAGES C
        HOMEPAGE_URL https://github.com/OpenCyphal/nunavut
        DESCRIPTION "Demonstration of running Nunavut from source using the CMAKE FetchContent module."
)

# This example demonstrates how to integrate Nunavut into a CMake project using the FetchContent module where only Cmake
# itself and Python are needed to run the code generation step. This is useful for projects that do not have a managed
# Python environment; allowing a default Python installation as might be found in a modern Linux distribution to be used
# without pip or setup-tools and without modifying the global Python environment. We do this by pulling Nunavut and
# Pydsdl from git and running Nunavut from source as ``python -m nunavut`` (which is the same as running the ``nnvg``
# script for an installed version of Nunavut).

# +---------------------------------------------------------------------------+
# | External Dependencies
# +---------------------------------------------------------------------------+
include(FetchContent)

# See CMakePresets.json for a convenient way to control online/offline mode.
# Documentation for this feature can be found here:
# https://cmake.org/cmake/help/latest/module/FetchContent.html#variable:FETCHCONTENT_FULLY_DISCONNECTED
if (${FETCHCONTENT_FULLY_DISCONNECTED})
    message(STATUS "☑️ FetchContent OFFLINE")
else()
    message(STATUS "✅ FetchContent ONLINE")
endif()

# We'll use LOCAL_EXTERNAL_ROOT by convention to refer to a folder under which all FetchContent projects will be stored.
# You can add this to .gitignore to avoid checking in the external source or you can check it in based on your project's
# needs. If checked in then the build will not need network access nor will it fail if github is unavailable. Because of
# this we highly recommend such a configuration. See the provided CMakePresets.json in this folder for a convenient way
# to switch between online and offline builds. This file defines presets such that:
#
#    cmake --preset Connected
#
# will configure the build and go online to synchronize resources where:
#
#    cmake --preset Disconnected
#
# will configure the build to stay offline and use whatever is available under the external folder.
set(LOCAL_EXTERNAL_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/external)

# Add Nunavut as a CMake module path. It contains the NunavutConfig.cmake file.
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/external/nunavut")

# We'll declare the Nunavut git repository in Github as the source we use in this example. From there we get all of
# Nunavut and submodules under `${LOCAL_EXTERNAL_ROOT}/nunavut/submodules` or just `submodules/` when the path is
# relative to the Nunavut repo itself.
FetchContent_Declare(
    Nunavut
    GIT_REPOSITORY  "https://github.com/OpenCyphal/nunavut"
    GIT_TAG         "b5053f55cb65432294d8c4730d5381b7bfd64a19" # <-- Here we've specified a specific git hash. This is
                                                               #     the strongest declaration of a fetched dependency.
                                                               #     Using a hash says we don't care if there are bugs
                                                               #     that are patched later. We want total build
                                                               #     reproducibility (caveat: https://github.blog/news-insights/company-news/sha-1-collision-detection-on-github-com/)
    SOURCE_DIR      "${LOCAL_EXTERNAL_ROOT}/nunavut"
    GIT_SUBMODULES
        submodules/public_regulated_data_types                 # <-- You can use the public regulated types in Nunavut
                                                               #     or you can pull your version in as a separate
                                                               #     FetchContent_Declare call. If you pull your own
                                                               #     be sure to set OMIT_PUBLIC_REGULATED_NAMESPACE
                                                               #     when calling add_cyphal_library.
        submodules/pydsdl                                      # <-- By pulling in pydsdl from Nunavut we get the
                                                               #     correct version and `add_cyphal_library` (see
                                                               #     below) automatically finds it. If you want to
                                                               #     pull in your own version `add_cyphal_library`
                                                               #     lets you change the path using the PYDSDL_PATH
                                                               #     argument.
    GIT_SHALLOW ON
    GIT_SUBMODULES_RECURSE OFF
    FIND_PACKAGE_ARGS 3.0
)

# Now we'll make the dependencies available. If FETCHCONTENT_FULLY_DISCONNECTED is set to OFF then this will pull the
# dependencies from github. As mentioned above, we recommend checking in these dependencies and committing them to your
# own repository to maximize build availability and reproducibility but to also run an online version of your build in a
# CI pipeline to ensure ongoing compatibility.
FetchContent_MakeAvailable(
    Nunavut
)

# +---------------------------------------------------------------------------+
# | Using NunavutConfig.cmake
# +---------------------------------------------------------------------------+

# We'll define the custom types we want to generate code for. These are the types that are not part of the public
# regulated data types. In this case we have two custom types that are part of the ecorp namespace.
set(LOCAL_ECORP_CUSTOM_TYPES
    ${CMAKE_CURRENT_SOURCE_DIR}/custom_types/ecorp/customer/record.2.8.dsdl
    ${CMAKE_CURRENT_SOURCE_DIR}/custom_types/ecorp/fintech/mortgage/property.4.2.dsdl
)

# Now we'll add a library target that will also setup a code gen target as a dependency. We'll add the public
# regulated types as a dependency so that the generated code can use those. This will generate code only for the
# custom types we've defined and their dependencies.
add_cyphal_library(
    NAME ecorp      # <------------------------------------------ Make sure this is unique for each cyphal library you
                    #                                             define, if you are defining more than one.
    DSDL_FILES ${LOCAL_ECORP_CUSTOM_TYPES}
    DSDL_NAMESPACES # <------------------------------------------ Here we list all valid namespace roots. Any direct
                    #                                             or dependent type that cannot be found under one of
                    #                                             these roots will cause the code-gen rule to fail.
                    #                                             The "uavcan" namespace under public_regulated_types
                    #                                             is automatically added to this list unless we set the
                    #                                             OMIT_PUBLIC_REGULATED_NAMESPACE option.
        ${CMAKE_CURRENT_SOURCE_DIR}/custom_types/ecorp
    LANGUAGE_STANDARD c11
    LANGUAGE c
    OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/include    # <-------- This is also the include path added to the interface
                                                      #           library for including generated code.
    OUT_LIBRARY_TARGET CYPHAL_GENERATED_HEADERS_ECORP # <-------- ${CYPHAL_GENERATED_HEADERS_ECORP} will resolve to the
                                                      #           name of the interface library defined after this
                                                      #           function exits (successfully).
    EXPORT_CONFIGURE_MANIFEST ${CMAKE_CURRENT_BINARY_DIR} # <---- Optional. Writes a configure_commands.json file under
                                                      #           this directory that dumps all configuration used by
                                                      #           Nunavut for the cmake configure step. (think of this
                                                      #           as the nnvg equivalent of compile_commands.json).
    EXPORT_GENERATE_MANIFEST ${CMAKE_CURRENT_BINARY_DIR} # <----- Optional. Writes a ${OUT_CODEGEN_TARGET}.json file
                                                      #           under this directory which includes all template
                                                      #           files, all dsdl files, and all output files that were
                                                      #           generated when a given code generation rule was
                                                      #           executed. Note that this value may use generator
                                                      #           expressions which is useful for Ninja-Multi-Config.
)


# +---------------------------------------------------------------------------+
# | Example Application
# +---------------------------------------------------------------------------+
# By way of demonstration, we'll add a little executable...
add_executable(ecorp_pi main.c)

# We then add a link to the headers interface library so the ecorp_pi executable will inherit the paths to the generated
# headers. This also ensures the code generation step will occur before the ecorp_pi compilation step. Note that, while
# this example uses dynamic resolution of all dsdl resources at configure-time, code generation is deferred to
# build-time.
target_link_libraries(ecorp_pi PUBLIC ${CYPHAL_GENERATED_HEADERS_ECORP})

# Using the provided presets do:
#
#   cmake --build --preset BuildDebug
#
# or
#
#   cmake --build --preset BuildRelease
#

CMakeLists.txt

CMake Presets Figure

This isn’t required but the following presets file demonstrates how you can use CMake presets to easily switch between offline and online builds when using CMake’s FetchContent module. See the CMakeLists Figure figure for more context.

{
    "version": 7,
    "cmakeMinimumRequired": {
        "major": 3,
        "minor": 27,
        "patch": 0
    },
    "configurePresets": [
        {
            "name": "config-common",
            "hidden": true,
            "description": "Common configuration",
            "generator": "Ninja Multi-Config",
            "binaryDir": "${sourceDir}/build",
            "warnings": {
                "deprecated": true,
                "uninitialized": true
            },
            "cacheVariables": {
                "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
                "CMAKE_CONFIGURATION_TYPES": "Release;Debug",
                "CMAKE_CROSS_CONFIGS": "all",
                "CMAKE_DEFAULT_BUILD_TYPE": "Release",
                "CMAKE_DEFAULT_CONFIGS": "Release",
                "CMAKE_PREFIX_PATH": "${sourceDir}/external/nunavut"
            }
        },
        {
            "name": "config-connected",
            "hidden": true,
            "cacheVariables": {
                "FETCHCONTENT_FULLY_DISCONNECTED": "OFF",
                "FETCHCONTENT_QUIET": "OFF"
            }
        },
        {
            "name": "config-disconnected",
            "hidden": true,
            "cacheVariables": {
                "FETCHCONTENT_FULLY_DISCONNECTED": "ON"
            }
        },
        {
            "name": "Connected",
            "displayName": "Connected Config",
            "description": "FetchContent will go online to look for updates when configured.",
            "inherits": [
                "config-common",
                "config-connected"
            ]
        },
        {
            "name": "Disconnected",
            "displayName": "Disconnected Config",
            "description": "FetchContent will not go online but will use any available local content to configure.",
            "inherits": [
                "config-common",
                "config-disconnected"
            ]
        }
    ],
    "buildPresets": [
        {
            "name": "BuildRelease",
            "displayName": "Nunavut cmake integration demo (Release)",
            "description": "Builds our silly little demo binary.",
            "configurePreset": "Disconnected",
            "configuration": "Release",
            "targets": [
                "ecorp_pi"
            ]
        },
        {
            "name": "BuildDebug",
            "displayName": "Nunavut cmake integration demo (Debug)",
            "description": "Builds our silly little demo binary.",
            "configurePreset": "Disconnected",
            "configuration": "Debug",
            "targets": [
                "ecorp_pi"
            ]
        }
    ]
}

CMakePresets.json

NunavutConfig

Use either CMake’s FetchContent (see FetchContent Example) or find_package(nunavut), to load the Nunavut cmake functions and variables documented here into your project.

NUNAVUT_SUBMODULES_DIR

Set by the Nunavut package, this is the path to the submodules folder in the nunavut repository.

NUNAVUT_EXTRA_GENERATOR_ARGS

If defined, this environment variable is used as additional command-line arguments which are passed to Nunavut when generating code. This can also be specified as a cache variable (e.g. cmake -DNUNAVUT_EXTRA_GENERATOR_ARGS) which will override any value set in the environment. As an environment variable, this list of args must use the system’s list separator (NUNAVUT_PATH_LIST_SEP) to specify multiple arguments. As a cache variable, cmake’s semicolon list separator must be used.

NUNAVUT_PATH_LIST_SEP

Platform-specific list separator determed by the Nunavut package and used to parse lists read from the environment or form lists set in the environment. Users shouldn’t need to use this variable but it can be overridden if the Nunavut cmake package’s automatic detection is incorrect.

discover_inputs_and_outputs

Invoke the nunavut CLI to discover all dsdl inputs for a given set of namespaces and the outputs that these would generate from a codegen build step.

Note

The add_cyphal_library() function uses this method internally so it is not necessary to use this method if defining a library using that function.

  • param LANGUAGE str:

    The language to generate code for. Supported types are c and cpp.

  • param DSDL_FILES list[path]:

    A list of DSDL files to generate code for.

  • param DSDL_NAMESPACES optional list[path]:

    A list of namespaces to search for dependencies in. Unless OMIT_PUBLIC_REGULATED_NAMESPACE is set, this will always include ${NUNAVUT_SUBMODULES_DIR}/public_regulated_data_types/uavcan

  • param LANGUAGE_STANDARD optional str:

    The language standard to use.

  • param OUTPUT_DIR optional path:

    The directory to write generated code to. If omitted then ${CMAKE_CURRENT_BINARY_DIR}/generated is used.

  • param CONFIGURATION optional list[path]:

    A list of configuration files to pass into nunavut. See the nunavut documentation for more information about configuration files.

  • param WORKING_DIRECTORY optional path:

    The working directory to use when invoking the Nunavut tool. If omitted then ${CMAKE_CURRENT_SOURCE_DIR} is used.

  • param PYDSDL_PATH optional path:

    The path to the PyDSDL tool. If omitted then this is set to ${NUNAVUT_SUBMODULES_DIR}/pydsdl which is the root of the pydsdl submodule in the Nunavut repo.

  • param FILE_EXTENSION optional str:

    The file extension to use for generated files. If omitted then the default for the language is used.

  • param EXPORT_CONFIGURE_MANIFEST optional path:

    A folder under which a json file containing a list of all the inputs, outputs, and other information about the configure-step execution of nunavut will be written to. This file will be named ${EXPORT_CONFIGURE_MANIFEST}/generate_commands.json and cannot contain generator expressions as this file is created at configure-time.

  • option ALLOW_EXPERIMENTAL_LANGUAGES:

    If set then unsupported languages will be allowed.

  • option CONSOLE_DEBUG:

    If set then verbose output will be enabled.

  • option SUPPORT_ONLY:

    If set then the library created will contain only support code needed to use the code generated for DSDL_FILES. This allows different cyphal libraries to share a single set of support headers and avoids duplicate target rules. This option is mutually exclusive with NO_SUPPORT.

  • option NO_SUPPORT:

    If set then the library created will not contain support code needed to use the code generated for DSDL_FILES. This is a mutually exclusive option with SUPPORT_ONLY.

  • option OMIT_PUBLIC_REGULATED_NAMESPACE:

    By default, ${NUNAVUT_SUBMODULES_DIR}/public_regulated_data_types/uavcan is added to the list of DSDL_NAMESPACES even if this variable is not set. This option disables this behaviour so only explicitly listed DSDL_NAMESPACES values will be used.

  • option OMIT_DEPENDENCIES:

    Disables the generation of dependent types. This is useful when setting up build rules for a project where the dependent types are generated separately.

  • param OUT_MANIFEST_DATA optional variable:

    If set, this method writes a variable named ${OUT_MANIFEST_DATA} with the json string containing the entire manifest read in from the nunavut CLI invocation.

  • param OUT_INPUTS_LIST optional variable:

    If set, this method writes a variable named ${OUT_LIBRARY_TARGET} with the interface library target name defined for the library in the calling scope.

  • param OUT_OUTPUTS_LIST optional variable:

    If set, this method writes a variable named ${OUT_CODEGEN_TARGET} with the custom target name defined for invoking the code generator.

add_cyphal_library

Create a library built from code generated by the Nunavut tool from dsdl files. This version of the function always defines an interface library since c and c++ types are generated as header-only.

Note

See the FetchContent Example for more guidance on using this function.

  • param NAME str:

    A name for the library. If EXACT_NAME is set then this is the exact name of the target. Otherwise, the target name will be derived from this name for uniqueness. Use OUT_LIBRARY_TARGET to capture the generated name of the library target.

  • param LANGUAGE str:

    The language to generate code for. Supported types are c and cpp.

  • param DSDL_FILES list[path]:

    A list of DSDL files to generate code for.

  • param DSDL_NAMESPACES optional list[path]:

    A list of namespaces to search for dependencies in. While optional, it’s rare that this would be omitted.

  • param LANGUAGE_STANDARD optional str:

    The language standard to use.

  • param OUTPUT_DIR optional path:

    The directory to write generated code to. If omitted then ${CMAKE_CURRENT_BINARY_DIR}/generated is used.

  • param EXPORT_CONFIGURE_MANIFEST optional path:

    A folder under which a json file containing a list of all the inputs, outputs, and other information about the configure-step execution of nunavut will be written to. This file will be named ${EXPORT_CONFIGURE_MANIFEST}/${OUT_CODEGEN_TARGET}.json and cannot contain generator expressions as this file is created at configure-time (see EXPORT_GENERATE_MANIFEST for the compile-time equivalent).

  • param EXPORT_GENERATE_MANIFEST optional path:

    A folder under which a json file containing a list of all the inputs, outputs, and other information about the code-generation-step execution of nunavut will be written to. This file will be named ${EXPORT_GENERATE_MANIFEST}/${OUT_CODEGEN_TARGET}.json and may contain generator expressions as this file is created only when executing the code gen build rule.

  • param CONFIGURATION optional list[path]:

    A list of configuration files to pass into nunavut. See the nunavut documentation for more information about configuration files.

  • param WORKING_DIRECTORY optional path:

    The working directory to use when invoking the Nunavut tool. If omitted then ${CMAKE_CURRENT_SOURCE_DIR} is used.

  • param PYDSDL_PATH optional path:

    The path to the PyDSDL tool. If omitted then this is set to ${NUNAVUT_SUBMODULES_DIR}/pydsdl which is the root of the pydsdl submodule in the Nunavut repo.

  • param FILE_EXTENSION optional str:

    The file extension to use for generated files. If omitted then the default for the language is used.

  • param EXTRA_GENERATOR_ARGS optional list[str]:

    Additional command-line arguments to pass to the Nunavut CLI when generating code. These args are not used for invoking the Nunavut CLI to discover dependencies.

    These are combined with any arguments specified by a NUNAVUT_EXTRA_GENERATOR_ARGS environment variable.

  • option ALLOW_EXPERIMENTAL_LANGUAGES:

    If set then unsupported languages will be allowed.

  • option CONSOLE_DEBUG:

    If set then verbose output will be enabled.

  • option SUPPORT_ONLY:

    If set then the library created will contain only support code needed to use the code generated for DSDL_FILES. This allows different cyphal libraries to share a single set of support headers and avoids duplicate target rules. This option is mutually exclusive with NO_SUPPORT.

  • option NO_SUPPORT:

    If set then the library created will not contain support code needed to use the code generated for DSDL_FILES. This is a mutually exclusive option with SUPPORT_ONLY.

  • option EXACT_NAME:

    If set then the target name will be exactly as specified in NAME. Otherwise, the target name will be prefixed with an internal default.

  • option OMIT_PUBLIC_REGULATED_NAMESPACE:

    By default, ${NUNAVUT_SUBMODULES_DIR}/public_regulated_data_types/uavcan is added to the list of DSDL_NAMESPACES even if this variable is not set. This option disables this behaviour so only explicitly listed DSDL_NAMESPACES values will be used.

  • option, mutually exclusive (one of ENDIAN_ANY | ENDIAN_LITTLE | ENDIAN_BIG):

    If one of these is set then the endianness argument is passed into the nunavut CLI otherwise endianess is taken from language configuration.

  • option OMIT_DEPENDENCIES:

    Disables the generation of dependent types. This is useful when setting up build rules for a project where the dependent types are generated separately.

  • param OUT_LIBRARY_TARGET optional variable:

    If set, this method write a variable named ${OUT_LIBRARY_TARGET} with the interface library target name defined for the library in the calling scope.

  • param OUT_CODEGEN_TARGET optional variable:

    If set, this method write a variable named ${OUT_CODEGEN_TARGET} with the custom target name defined for invoking the code generator.