深入理解CMake(5):find_package寻找手动编译安装的Protobuf过程分析

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 
#include 
#include 
#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 
#include 
#include 
#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寻找手动编译安装的Protobuf过程分析)