| Step 10: Finding Dependencies |
| ============================= |
| |
| In C/C++ software development, managing build dependencies is consistently |
| one of the highest ranked challenges facing modern developers. CMake provides |
| an extensive toolset for discovering and validating dependencies of different |
| kinds. |
| |
| However, for correctly packaged projects there is no need to use these advanced |
| tools. Many popular library and utility projects today produce correct install |
| trees, like the one we set up in ``Step 9``, which are easy is to integrate |
| into CMake. |
| |
| In this best-case scenario, we only need the :command:`find_package` to |
| import dependencies into our project. |
| |
| Background |
| ^^^^^^^^^^ |
| |
| There are five principle commands used for discovering dependencies with |
| CMake, the first four are: |
| |
| :command:`find_file` |
| Finds and reports the full path to a named file, this tends to be the |
| most flexible of the ``find`` commands. |
| |
| :command:`find_library` |
| Finds and reports the full path to a static archive or shared object |
| suitable for use with :command:`target_link_libraries`. |
| |
| :command:`find_path` |
| Finds and reports the full path to a directory *containing* a file. This |
| is most commonly used for headers in combination with |
| :command:`target_include_directories`. |
| |
| :command:`find_program` |
| Finds and reports and invocable name or path for a program. Often used in |
| combination with :command:`execute_process` or :command:`add_custom_command`. |
| |
| These commands should be considered "backup", used when the primary find command |
| is unsuitable. The primary find command is :command:`find_package`. It uses |
| comprehensive built-in heuristics and upstream-provided packaging files to |
| provide the best interface to the requested dependency. |
| |
| Exercise 1 - Using ``find_package()`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| The search paths and behaviors used by :command:`find_package` are fully |
| described in its documentation, but much too verbose to replicate here. Suffice |
| to say it searches well known, lesser known, obscure, and user-provided |
| locations attempting to find a package which meets the requirements given to it. |
| |
| .. code-block:: cmake |
| |
| find_package(ForeignLibrary) |
| |
| The best way to use :command:`find_package` is to ensure all dependencies have |
| been installed to a single install tree prior to the build, and then make the |
| location of that install tree known to :command:`find_package` via the |
| :variable:`CMAKE_PREFIX_PATH` variable. |
| |
| .. note:: |
| Building and installing dependencies can itself be an immense amount of labor. |
| While this tutorial will do so for illustration purposes, it is **extremely** |
| recommended that a package manager be used for project-local dependency |
| management. |
| |
| :command:`find_package` accepts several parameters besides the package to be |
| found. The most notable are: |
| |
| * A positional ``<version>`` argument, for describing a version to be checked |
| against the package's config version file. This should be used sparingly, |
| it is better to control the version of the dependency being installed via |
| a package manager than possibly break the build on otherwise innocuous |
| version updates. |
| |
| If the package is known to rely on an older version of a dependency, it |
| may be appropriate to use a version requirement. |
| |
| * ``REQUIRED`` for non-optional dependencies which should abort the build |
| if not found. |
| |
| * ``QUIET`` for optional dependencies which should not report anything to |
| users when not found. |
| |
| :command:`find_package` reports its results via ``<PackageName>_FOUND`` |
| variables, which will be set to a true or false value for found and not found |
| packages respectively. |
| |
| Goal |
| ---- |
| |
| Integrate an externally installed test framework into the Tutorial project. |
| |
| Helpful Resources |
| ----------------- |
| |
| * :command:`find_package` |
| * :command:`target_link_libraries` |
| |
| Files to Edit |
| ------------- |
| |
| * ``TutorialProject/CMakePresets.json`` |
| * ``TutorialProject/Tests/CMakeLists.txt`` |
| * ``TutorialProject/Tests/TestMathFunctions.cxx`` |
| |
| Getting Started |
| --------------- |
| |
| The ``Step10`` folder is organized differently than previous steps. The tutorial |
| project we need to edit is under ``Step10/TutorialProject``. Another project |
| is now present, ``SimpleTest``, as well as a partially populated install tree |
| which we will use in later exercises. You do not need to edit anything in these |
| other directories for this exercise, all ``TODOs`` and solution steps are for |
| ``TutorialProject``. |
| |
| The ``SimpleTest`` package provides two useful constructs, the |
| ``SimpleTest::SimpleTest`` target to be linked into a test binary, and the |
| ``simpletest_discover_tests`` function for automatically adding tests to |
| CTest. |
| |
| Similar to other test frameworks, ``simpletest_discover_tests`` only needs |
| to be passed the name of the executable target containing the tests. |
| |
| .. code-block:: cmake |
| |
| simpletest_discover_tests(MyTestExe) |
| |
| The ``TestMathFunctions.cxx`` file has been updated to use the ``SimpleTest`` |
| framework in the vein of GoogleTest or Catch2. Perform ``TODO 1`` through |
| ``TODO 5`` in order to use the new test framework. |
| |
| .. note:: |
| It may go without saying, but ``SimpleTest`` is a very poor test framework |
| which only facially resembles a functional testing library. While much of |
| the CMake code in this tutorial could be used unaltered in other projects, |
| you should not use ``SimpleTest`` outside this tutorial, or try to learn from |
| the CMake code it provides. |
| |
| Build and Run |
| ------------- |
| |
| First we must install the ``SimpleTest`` framework. Navigate to the |
| ``Help/guide/Step10/SimpleTest`` directory and run the following commands |
| |
| .. code-block:: console |
| |
| cmake --preset tutorial |
| cmake --install build |
| |
| .. note:: |
| The ``SimpleTest`` preset sets up everything needed to install ``SimpleTest`` |
| for the tutorial. For reasons that are beyond the scope of this tutorial, |
| there is no need to build or provide any other configuration for |
| ``SimpleTest``. |
| |
| We can observe that the ``Step10/install`` directory has now been populated by |
| the ``SimpleTest`` header and package files. |
| |
| Now we can configure and build the Tutorial project as per usual, navigating to |
| the ``Help/guide/Step10/TutorialProject`` and running: |
| |
| .. code-block:: console |
| |
| cmake --preset tutorial |
| cmake --build build |
| |
| Verify that the ``SimpleTest`` framework has been consumed correctly by running |
| the tests with CTest. |
| |
| Solution |
| -------- |
| |
| First we call :command:`find_package` to discover the ``SimpleTest`` package. |
| We do this with ``REQUIRED`` because the tests cannot build without |
| ``SimpleTest``. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 1 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tests/CMakeLists.txt |
| :caption: TODO 1: TutorialProject/Tests/CMakeLists.txt |
| :name: TutorialProject/Tests/CMakeLists.txt-find_package |
| :language: cmake |
| :start-at: find_package |
| :end-at: find_package |
| |
| .. raw:: html |
| |
| </details> |
| |
| Next we add the ``SimpleTest::SimpleTest`` target to ``TestMathFunctions`` |
| |
| .. raw:: html |
| |
| <details><summary>TODO 2 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tests/CMakeLists.txt |
| :caption: TODO 2: TutorialProject/Tests/CMakeLists.txt |
| :name: TutorialProject/Tests/CMakeLists.txt-link-simple-test |
| :language: cmake |
| :start-at: target_link_libraries(TestMathFunctions |
| :end-at: ) |
| |
| .. raw:: html |
| |
| </details> |
| |
| Now we can replace our test description code with a call to |
| ``simpletest_discover_tests``. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 3 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tests/CMakeLists.txt |
| :caption: TODO 3: TutorialProject/Tests/CMakeLists.txt |
| :name: TutorialProject/Tests/CMakeLists.txt-simpletest_discover_tests |
| :language: cmake |
| :start-at: simpletest_discover_tests |
| :end-at: simpletest_discover_tests |
| |
| .. raw:: html |
| |
| </details> |
| |
| We ensure :command:`find_package` can discover ``SimpleTest`` by |
| adding the install tree to :variable:`CMAKE_PREFIX_PATH`. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 4 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/CMakePresets.json |
| :caption: TODO 4: TutorialProject/CMakePresets.json |
| :name: TutorialProject/CMakePresets.json-CMAKE_PREFIX_PATH |
| :language: json |
| :start-at: cacheVariables |
| :end-at: TUTORIAL_ENABLE_IPO |
| :dedent: 6 |
| :append: } |
| |
| .. raw:: html |
| |
| </details> |
| |
| Finally, we update the tests to use the macros provided by ``SimpleTest`` by |
| removing the placeholders and including the appropriate header. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 5 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tests/TestMathFunctions.cxx |
| :caption: TODO 5: TutorialProject/Tests/TestMathFunctions.cxx |
| :name: TutorialProject/Tests/TestMathFunctions.cxx-simpletest |
| :language: c++ |
| :start-at: #include <MathFunctions.h> |
| :end-at: { |
| |
| .. raw:: html |
| |
| </details> |
| |
| Exercise 2 - Transitive Dependencies |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| Libraries often build on one another. A multimedia application may depend on a |
| library which provides support for various container formats, which may in turn |
| rely on one or more other libraries for compression algorithms. |
| |
| We need to express these transitive requirements inside the package config |
| files we place in the install tree. We do so with the |
| :module:`CMakeFindDependencyMacro` module, which provides a safe mechanism for |
| installed packages to recursively discover one another. |
| |
| .. code-block:: cmake |
| |
| include(CMakeFindDependencyMacro) |
| find_dependency(zlib) |
| |
| :module:`find_dependency() <CMakeFindDependencyMacro>` also forwards arguments |
| from the top-level :command:`find_package` call. If :command:`find_package` is |
| called with ``QUIET`` or ``REQUIRED``, |
| :module:`find_dependency() <CMakeFindDependencyMacro>` will also use ``QUIET`` |
| and/or ``REQUIRED``. |
| |
| Goal |
| ---- |
| |
| Add a dependency to ``SimpleTest`` and ensure that packages which rely on |
| ``SimpleTest`` also discover this transitive dependency. |
| |
| Helpful Resources |
| ----------------- |
| |
| * :module:`CMakeFindDependencyMacro` |
| * :command:`find_package` |
| * :command:`target_link_libraries` |
| |
| Files to Edit |
| ------------- |
| |
| * ``SimpleTest/CMakeLists.txt`` |
| * ``SimpleTest/cmake/SimpleTestConfig.cmake`` |
| |
| Getting Started |
| --------------- |
| |
| For this step we will only be editing the ``SimpleTest`` project. The transitive |
| dependency, ``TransitiveDep``, is a dummy dependency which provides no behavior. |
| However CMake doesn't know this and the ``TutorialProject`` tests will fail to |
| configure and build if CMake cannot find all required dependencies. |
| |
| The ``TransitiveDep`` package has already been installed to the |
| ``Step10/install`` tree. We do not need to install it as we did with |
| ``SimpleTest``. |
| |
| Complete ``TODO 6`` through ``TODO 8``. |
| |
| Build and Run |
| ------------- |
| |
| We need to reinstall the SimpleTest framework. Navigate to the |
| ``Help/guide/Step10/SimpleTest`` directory and run the same commands as before. |
| |
| .. code-block:: console |
| |
| cmake --preset tutorial |
| cmake --install build |
| |
| Now we can reconfigure and rebuild the ``TutorialProject``, navigate to |
| ``Help/guide/Step10/TutorialProject`` and perform the usual steps to do so. |
| |
| .. code-block:: console |
| |
| cmake --preset tutorial |
| cmake --build build |
| |
| If the build passed we have likely successfully propagated the transitive |
| dependency. Verify this by searching the ``CMakeCache.txt`` of |
| ``TutorialProject`` for an entry named ``TransitiveDep_DIR``. This demonstrates |
| the ``TutorialProject`` searched for an found ``TransitiveDep`` even though it |
| has no direct requirement for it. |
| |
| Solution |
| -------- |
| |
| First we call :command:`find_package` to discover the ``TransitiveDep`` package. |
| We use ``REQUIRED`` to verify we have found ``TransitiveDep``. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 6 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/SimpleTest/CMakeLists.txt |
| :caption: TODO 6: SimpleTest/CMakeLists.txt |
| :name: SimpleTest/CMakeLists.txt-find_package |
| :language: cmake |
| :start-at: find_package |
| :end-at: find_package |
| |
| .. raw:: html |
| |
| </details> |
| |
| Next we add the ``TransitiveDep::TransitiveDep`` target to ``SimpleTest``. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 7 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/SimpleTest/CMakeLists.txt |
| :caption: TODO 7: SimpleTest/CMakeLists.txt |
| :name: SimpleTest/CMakeLists.txt-link-transitive-dep |
| :language: cmake |
| :start-at: target_link_libraries(SimpleTest |
| :end-at: ) |
| |
| .. raw:: html |
| |
| </details> |
| |
| .. note:: |
| If we built ``TutorialProject`` at this point, we would expect the |
| configuration to fail due to the ``TransitiveDep::TransitiveDep`` target |
| being unavailable inside that project. |
| |
| Finally, we include the :module:`CMakeFindDependencyMacro` and call |
| :module:`find_dependency() <CMakeFindDependencyMacro>` inside the ``SimpleTest`` |
| package config file to propagate the transitive dependency. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 8 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/SimpleTest/cmake/SimpleTestConfig.cmake |
| :caption: TODO 8: SimpleTest/cmake/SimpleTestConfig.cmake |
| :name: SimpleTest/cmake/SimpleTestConfig.cmake-find_dependency |
| :language: cmake |
| :start-at: include |
| :end-at: find_dependency |
| |
| .. raw:: html |
| |
| </details> |
| |
| </details> |
| |
| Exercise 3 - Finding Other Kinds of Files |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| In a perfect world every dependency we care about would be packaged correctly, |
| or at least some other developer would have written a module that discovers it |
| for us. We do not live in a perfect world, and sometimes we will have to get |
| our hands dirty and discover build requirements manually. |
| |
| For this we have the other find commands enumerated earlier in the step, such |
| as :command:`find_path`. |
| |
| .. code-block:: cmake |
| |
| find_path(PackageIncludeFolder Package.h REQUIRED |
| PATH_SUFFIXES |
| Package |
| ) |
| target_include_directories(MyApp |
| PRIVATE |
| ${PackageIncludeFolder} |
| ) |
| |
| Goal |
| ---- |
| |
| Add an unpackaged header to the ``Tutorial`` executable of the |
| ``TutorialProject``. |
| |
| Helpful Resources |
| ----------------- |
| |
| * :command:`find_path` |
| * :command:`target_include_directories` |
| |
| Files to Edit |
| ------------- |
| |
| * ``TutorialProject/Tutorial/CMakeLists.txt`` |
| * ``TutorialProject/Tutorial/Tutorial.cxx`` |
| |
| Getting Started |
| --------------- |
| |
| For this step we will only be editing the ``TutorialProject`` project. The |
| unpackaged header, ``Unpackaged/Unpackaged.h`` has already been installed to the |
| ``Step10/install`` tree. |
| |
| Complete ``TODO 9`` through ``TODO 11``. |
| |
| Build and Run |
| ------------- |
| |
| There are no special build steps for this exercise, navigate to |
| ``Help/guide/Step10/TutorialProject`` and perform the usual build. |
| |
| .. code-block:: console |
| |
| cmake --build build |
| |
| If the build passed we have successfully added the ``Unpackaged`` include |
| directory to the project. |
| |
| Solution |
| -------- |
| |
| First we call :command:`find_path` to discover the ``Unpackaged`` include |
| directory. We use ``REQUIRED`` because building ``Tutorial`` will fail if |
| we cannot locate the ``Unpackaged.h`` header. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 9 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tutorial/CMakeLists.txt |
| :caption: TODO 9: TutorialProject/Tutorial/CMakeLists.txt |
| :name: TutorialProject/Tutorial/CMakeLists.txt-find_path |
| :language: cmake |
| :start-at: find_path |
| :end-at: ) |
| |
| .. raw:: html |
| |
| </details> |
| |
| Next we add the discovered path to ``Tutorial`` using |
| :command:`target_include_directories`. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 10 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tutorial/CMakeLists.txt |
| :caption: TODO 10: TutorialProject/Tutorial/CMakeLists.txt |
| :name: TutorialProject/Tutorial/CMakeLists.txt-target_include_directories |
| :language: cmake |
| :start-at: target_include_directories |
| :end-at: ) |
| |
| .. raw:: html |
| |
| </details> |
| |
| Finally, we edit ``Tutorial.cxx`` to include the discovered header. |
| |
| .. raw:: html |
| |
| <details><summary>TODO 11 Click to show/hide answer</summary> |
| |
| .. literalinclude:: Step11/TutorialProject/Tutorial/Tutorial.cxx |
| :caption: TODO 11: TutorialProject/Tutorial/Tutorial.cxx |
| :name: TutorialProject/Tutorial/Tutorial.cxx-include-unpackaged |
| :language: c++ |
| :start-at: #include <MathFunctions.h> |
| :end-at: #include <Unpackaged.h> |
| |
| .. raw:: html |
| |
| </details> |