| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- 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>
|