美文网首页CMake实践
深入理解CMake(5):find_package寻找手动编译安

深入理解CMake(5):find_package寻找手动编译安

作者: BetterCV | 来源:发表于2020-05-29 20:18 被阅读0次
    package-153360_960_720.png

    先前分析了find_package()的原理,也分析了find_package()查找系统Protobuf(apt安装)的具体细节。这次来分析自行编译安装的Protobuf是如何被(没)找到、如何配置使得能被找到。

    环境:

    • ubuntu 16.04;
    • 执行了sudo apt remove libprotobuf-dev卸载protobuf;
    • 自行编译安装了protobuf到/home/zz/soft/protobuf-3.8.0

    1. Protobuf的头文件目录

    首先我们知道cmake安装目录下提供了FindProtobuf.cmake,因此find_package(Protobuf)一定是在MODULE模式下而不是CONFIG模式下被搜索到的。(题外话:现代的cmake推荐用XXXConfig.cmake也就是CONFIG模式来找依赖包,这方面OpenCV可以作为典范写的确实越来越好)。

    在CMakeLists.txt中做查找:

    find_package(Protobuf REQUIRED)
    

    提示报错:

    -- The C compiler identification is GNU 5.4.0
    -- The CXX compiler identification is GNU 5.4.0
    -- Check for working C compiler: /usr/bin/cc
    -- Check for working C compiler: /usr/bin/cc - works
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Check for working CXX compiler: /usr/bin/c++
    -- Check for working CXX compiler: /usr/bin/c++ - works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    CMake Error at /home/zz/soft/cmake/share/cmake-3.17/Modules/FindPackageHandleStandardArgs.cmake:164 (message):
      Could NOT find Protobuf (missing: Protobuf_INCLUDE_DIR)
    Call Stack (most recent call first):
      /home/zz/soft/cmake/share/cmake-3.17/Modules/FindPackageHandleStandardArgs.cmake:445 (_FPHSA_FAILURE_MESSAGE)
      /home/zz/soft/cmake/share/cmake-3.17/Modules/FindProtobuf.cmake:626 (FIND_PACKAGE_HANDLE_STANDARD_ARGS)
      CMakeLists.txt:11 (find_package)
    
    
    -- Configuring incomplete, errors occurred!
    See also "/home/zz/work/oh-my-cmake/build/CMakeFiles/CMakeOutput.log".
    See also "/home/zz/work/oh-my-cmake/build/CMakeFiles/CMakeError.log".
    

    可以看出是Protobuf_INCLUDE_DIR变量为空,而这是由于/home/zz/soft/cmake/share/cmake-3.17/Modules/FindProtobuf.cmake没找到protobuf的头文件搜索目录。具体来说是这段调用:

    # Find the include directory
    find_path(Protobuf_INCLUDE_DIR
        google/protobuf/service.h
        PATHS ${Protobuf_SRC_ROOT_FOLDER}/src
    )
    mark_as_advanced(Protobuf_INCLUDE_DIR)
    

    find_path()并没有找到包含google/protobuf/service.h的目录,因为:1)我们用apt卸载了(或者说没有安装)apt仓库里的libprotobuf-dev;2)给find_path()传入的搜索参数也不能让它找到这样的目录。注意到FindProtobuf.cmake开头的多行注释中提到,可以设置(set)或使用(use)如下缓存变量(cache variable):

    ``Protobuf_LIBRARY``
      The protobuf library
    ``Protobuf_PROTOC_LIBRARY``
      The protoc library
    ``Protobuf_INCLUDE_DIR``
      The include directory for protocol buffers
    ``Protobuf_PROTOC_EXECUTABLE``
      The protoc compiler
    ``Protobuf_LIBRARY_DEBUG``
      The protobuf library (debug)
    ``Protobuf_PROTOC_LIBRARY_DEBUG``
      The protoc library (debug)
    ``Protobuf_LITE_LIBRARY``
      The protobuf lite library
    ``Protobuf_LITE_LIBRARY_DEBUG``
      The protobuf lite library (debug)
    

    因此,可以通过指定Protobuf_INCLUDE_DIR变量,来让find_package(Protobuf REQUIRED)正确的找到头文件目录(真是“多此一举”)。

    而根据前一篇对find_path()的第一条规则的了解,只要设定CMAKE_SYSTEM_PREFIX_PATH追加一个能找到google/protobuf/service.h的目录,就可以正确的产生Protobuf_INCLUDE_DIR变量。

    实测发现,CMAKE_SYSTEM_PREFIX_PATHCMAKE_PREFIX_PATH的设定,都可以影响find_path()。在本文的分析场景中,以下两种设定都可以让Protobuf_INCLUDE_PATH产生正确的值(但是库文件还是找不到的,暂时忽略):

    list(APPEND CMAKE_SYSTEM_PREFIX_PATH "/home/zz/soft/protobuf-3.8.0/include")
    message(STATUS "==== CMAKE_SYSTEM_PREFIX_PATH: ${CMAKE_SYSTEM_PREFIX_PATH}")
    find_package(Protobuf REQUIRED)
    
    list(APPEND CMAKE_PREFIX_PATH "/home/zz/soft/protobuf-3.8.0/include")
    message(STATUS "=== CMAKE_PREFIX_PATH is: ${CMAKE_PREFIX_PATH}")
    find_package(Protobuf REQUIRED)
    

    翻看了CMAKE_SYSTEM_PREFIX_PATH的文档页面,此变量是若干其它变量取值的拼接,不建议修改;鼓励修改CMAKE_PREFIX_PATH
    CMAKE_PREFIX_PATH的文档页面,则表明了它是用来在find_package(), find_library(), find_program(), find_file(), find_path()等命令中执行查找时提供prefix的选择。

    2. Protobuf的库文件

    本小结探究Protobuf的库文件被搜索到的过程。我们首先确保Protobuf的头文件搜索能被找到,这次选择在CMAKE_PREFIX_PATH里进行设定,对应的输出:

    -- The C compiler identification is GNU 5.4.0
    -- The CXX compiler identification is GNU 5.4.0
    -- Check for working C compiler: /usr/bin/cc
    -- Check for working C compiler: /usr/bin/cc - works
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Check for working CXX compiler: /usr/bin/c++
    -- Check for working CXX compiler: /usr/bin/c++ - works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- === CMAKE_PREFIX_PATH is: /home/zz/soft/protobuf-3.8.0/include
    -- Found Protobuf: Protobuf_LIBRARY-NOTFOUND;-lpthread (found version "3.8.0")
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /home/zz/work/oh-my-cmake/build

    注意其中Protobuf_LIBRARY-NOTFOUND;-lpthreadNOTFOUND字样,其实是protobuf库文件没找到的造成的。

    根据前面分析,以及文档中对CMAKE_PREFIX_PATH的说明,我们让CMAKE_PREFIX_PATH再增加一项,也就是protobuf的库文件所在目录

    list(APPEND CMAKE_PREFIX_PATH "/home/zz/soft/protobuf-3.8.0/include;/home/zz/soft/protobuf-3.8.0/lib")
    

    清理CMakeCache.txt后重新执行cmake,protobuf的库文件就能被正确的找到了,find_package(Protobuf REQUIRED)因而不再报错:

    -- Found Protobuf: /home/zz/soft/protobuf-3.8.0/lib/libprotobuf.a;-lpthread (found version "3.8.0")

    3. Protobuf可执行文件

    大多数用到Protobuf的C/C++工程,只需要find_protobuf(Protobuf)能提供头文件搜索目录、库文件绝对路径。

    但也有那么一小撮C/C++程序,还需要调用protobuf的编译器,也就是名为protoc的可执行文件。对于本文讨论的情况,我们并没有假设~/soft/protobuf-3.8.0/bin放在了PATH环境变量中。你可以放,但既然已经是手动编译的Protobuf了,也应该知道不在PATH里添加protoc所在目录的情况下,在CMakeLists.txt中进行设定的方式。

    依然是翻看FindProtobuf.cmake,发现可以手动指定Protobuf_PROTOC_EXECUTABLE这一缓存变量,不过这让人觉得多此一举。

    而在CMAKE_PREFIX_PATH的文档页中提到,它里面的值作为prefix可以用于find_program(),而FindProtobuf.cmake中对于protoc的查找正是基于find_program()实现的。因此仍然是在CMAKE_PREFIX_PATH中添加一项,来找到protoc。然而一次性塞了include目录、库目录、bin目录,比较臃肿,考虑用变量:

    set(Protobuf_PREFIX_PATH
        "/home/zz/soft/protobuf-3.8.0/include"
        "/home/zz/soft/protobuf-3.8.0/lib"
        "/home/zz/soft/protobuf-3.8.0/bin"
    )
    list(APPEND CMAKE_PREFIX_PATH "${Protobuf_PREFIX_PATH}")
    

    当然,实际的例子可能还需要额外的设定。来看具体的例子吧。

    4. Protobuf的一个实际例子

    这里例子中,既需要Protobuf的include目录、库文件路径,也需要protoc的可执行路径;而因为用了protobuf3.8,还需要开启C++11。

    目录结构:

    (base) arcsoft-43% tree
    .
    ├── CMakeLists.txt
    ├── proto
    │ └── addressbook.proto
    ├── src
    │ ├── protobuf_example_read.cpp
    │ └── protobuf_example_write.cpp
    └── utils.cmake

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.15)
    
    project(oh-my-cmake)
    
    set(CMAKE_CXX_STANDARD 11)
    
    set(Protobuf_PREFIX_PATH
        "/home/zz/soft/protobuf-3.8.0/include"
        "/home/zz/soft/protobuf-3.8.0/lib"
        "/home/zz/soft/protobuf-3.8.0/bin"
    )
    list(APPEND CMAKE_PREFIX_PATH "${Protobuf_PREFIX_PATH}")
    message(STATUS "=== CMAKE_PREFIX_PATH is: ${CMAKE_PREFIX_PATH}")
    
    set(protobuf_MODULE_COMPATIBLE ON CACHE BOOL "")
    find_package(Protobuf REQUIRED)
    #message(STATUS "=== Protobuf_PROTOC_EXECUTABLE: ${Protobuf_PROTOC_EXECUTABLE}")
    
    
    message(STATUS "=== Protobuf_INCLUDE_DIR is: ${Protobuf_INCLUDE_DIR}")
    message(STATUS "=== Protobuf_INCLUDE_DIRS is: ${Protobuf_INCLUDE_DIRS}")
    include_directories(${Protobuf_INCLUDE_DIRS})
    include_directories(${CMAKE_CURRENT_BINARY_DIR})
    
    protobuf_generate_cpp(AddressBook_PROTO_SRCS AddressBook_PROTO_HDRS proto/addressbook.proto)
    
    add_executable(protobuf_example_write src/protobuf_example_write.cpp ${AddressBook_PROTO_SRCS} ${AddressBook_PROTO_HDRS})
    add_executable(protobuf_example_read  src/protobuf_example_read.cpp  ${AddressBook_PROTO_SRCS} ${AddressBook_PROTO_HDRS})
    
    target_link_libraries(protobuf_example_write ${Protobuf_LIBRARIES})
    target_link_libraries(protobuf_example_read  ${Protobuf_LIBRARIES})
    

    src/protobuf_example_read.cpp

    #include <iostream>
    #include <fstream>
    #include <string>
    #include "addressbook.pb.h"
    using namespace std;
    
    // Iterates though all people in the AddressBook and prints info about them.
    void ListPeople(const tutorial::AddressBook& address_book) {
      for (int i = 0; i < address_book.people_size(); i++) {
        const tutorial::Person& person = address_book.people(i);
    
        cout << "Person ID: " << person.id() << endl;
        cout << "  Name: " << person.name() << endl;
        if (person.has_email()) {
          cout << "  E-mail address: " << person.email() << endl;
        }
    
        for (int j = 0; j < person.phones_size(); j++) {
          const tutorial::Person::PhoneNumber& phone_number = person.phones(j);
    
          switch (phone_number.type()) {
            case tutorial::Person::MOBILE:
              cout << "  Mobile phone #: ";
              break;
            case tutorial::Person::HOME:
              cout << "  Home phone #: ";
              break;
            case tutorial::Person::WORK:
              cout << "  Work phone #: ";
              break;
          }
          cout << phone_number.number() << endl;
        }
      }
    }
    
    // Main function:  Reads the entire address book from a file and prints all
    //   the information inside.
    int main(int argc, char* argv[]) {
      // Verify that the version of the library that we linked against is
      // compatible with the version of the headers we compiled against.
      GOOGLE_PROTOBUF_VERIFY_VERSION;
    
      if (argc != 2) {
        cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
        return -1;
      }
    
      tutorial::AddressBook address_book;
    
      {
        // Read the existing address book.
        fstream input(argv[1], ios::in | ios::binary);
        if (!address_book.ParseFromIstream(&input)) {
          cerr << "Failed to parse address book." << endl;
          return -1;
        }
      }
    
      ListPeople(address_book);
    
      // Optional:  Delete all global objects allocated by libprotobuf.
      google::protobuf::ShutdownProtobufLibrary();
    
      return 0;
    }
    

    src/protobuf_example_write.cpp

    #include <iostream>
    #include <fstream>
    #include <string>
    #include "addressbook.pb.h"
    using namespace std;
    
    // This function fills in a Person message based on user input.
    void PromptForAddress(tutorial::Person* person) {
      cout << "Enter person ID number: ";
      int id;
      cin >> id;
      person->set_id(id);
      cin.ignore(256, '\n');
    
      cout << "Enter name: ";
      getline(cin, *person->mutable_name());
    
      cout << "Enter email address (blank for none): ";
      string email;
      getline(cin, email);
      if (!email.empty()) {
        person->set_email(email);
      }
    
      while (true) {
        cout << "Enter a phone number (or leave blank to finish): ";
        string number;
        getline(cin, number);
        if (number.empty()) {
          break;
        }
    
        tutorial::Person::PhoneNumber* phone_number = person->add_phones();
        phone_number->set_number(number);
    
        cout << "Is this a mobile, home, or work phone? ";
        string type;
        getline(cin, type);
        if (type == "mobile") {
          phone_number->set_type(tutorial::Person::MOBILE);
        } else if (type == "home") {
          phone_number->set_type(tutorial::Person::HOME);
        } else if (type == "work") {
          phone_number->set_type(tutorial::Person::WORK);
        } else {
          cout << "Unknown phone type.  Using default." << endl;
        }
      }
    }
    
    // Main function:  Reads the entire address book from a file,
    //   adds one person based on user input, then writes it back out to the same
    //   file.
    int main(int argc, char* argv[]) {
      // Verify that the version of the library that we linked against is
      // compatible with the version of the headers we compiled against.
      GOOGLE_PROTOBUF_VERIFY_VERSION;
    
      if (argc != 2) {
        cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
        return -1;
      }
    
      tutorial::AddressBook address_book;
    
      {
        // Read the existing address book.
        fstream input(argv[1], ios::in | ios::binary);
        if (!input) {
          cout << argv[1] << ": File not found.  Creating a new file." << endl;
        } else if (!address_book.ParseFromIstream(&input)) {
          cerr << "Failed to parse address book." << endl;
          return -1;
        }
      }
    
      // Add an address.
      PromptForAddress(address_book.add_people());
    
      {
        // Write the new address book back to disk.
        fstream output(argv[1], ios::out | ios::trunc | ios::binary);
        if (!address_book.SerializeToOstream(&output)) {
          cerr << "Failed to write address book." << endl;
          return -1;
        }
      }
    
      // Optional:  Delete all global objects allocated by libprotobuf.
      google::protobuf::ShutdownProtobufLibrary();
    
      return 0;
    }
    

    proto/addressbook.proto

    syntax = "proto2";
    
    package tutorial;
    
    message Person {
      required string name = 1;
      required int32 id = 2;
      optional string email = 3;
    
      enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
      }
    
      message PhoneNumber {
        required string number = 1;
        optional PhoneType type = 2 [default = HOME];
      }
    
      repeated PhoneNumber phones = 4;
    }
    
    message AddressBook {
      repeated Person people = 1;
    }
    

    具体的构建过程:

    mkdir build
    cd build
    cmake ..
    make
    

    5. 总结

    通常,使用Protobuf作为依赖库的C/C++程序,并且Protobuf是自行编译安装的版本;设定CMAKE_PREFIX_PATH为同时包含protobuf的include目录、库目录,然后执行find_package(Protobuf)即可。

    个别复杂的,还需要添加可执行文件的目录到CMAKE_PREFIX_PATH。如果还是不够用(例如cmake正常而make阶段报错),则翻看FindProtobuf.cmake并结合CMake官方文档查阅即可。

    相关文章

      网友评论

        本文标题:深入理解CMake(5):find_package寻找手动编译安

        本文链接:https://www.haomeiwen.com/subject/ckwfzhtx.html