先前分析了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_PATH
和CMAKE_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;-lpthread
的NOTFOUND
字样,其实是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官方文档查阅即可。