
先前分析了find_package()的原理,也分析了find_package()查找系統(tǒng)Protobuf(apt安裝)的具體細(xì)節(jié)。這次來(lái)分析自行編譯安裝的Protobuf是如何被(沒(méi))找到、如何配置使得能被找到。
環(huán)境:
- ubuntu 16.04;
- 執(zhí)行了
sudo apt remove libprotobuf-dev卸載protobuf; - 自行編譯安裝了protobuf到
/home/zz/soft/protobuf-3.8.0
1. Protobuf的頭文件目錄
首先我們知道cmake安裝目錄下提供了FindProtobuf.cmake,因此find_package(Protobuf)一定是在MODULE模式下而不是CONFIG模式下被搜索到的。(題外話:現(xiàn)代的cmake推薦用XXXConfig.cmake也就是CONFIG模式來(lái)找依賴(lài)包,這方面OpenCV可以作為典范寫(xiě)的確實(shí)越來(lái)越好)。
在CMakeLists.txt中做查找:
find_package(Protobuf REQUIRED)
提示報(bào)錯(cuò):
-- 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沒(méi)找到protobuf的頭文件搜索目錄。具體來(lái)說(shuō)是這段調(diào)用:
# 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()并沒(méi)有找到包含google/protobuf/service.h的目錄,因?yàn)椋?)我們用apt卸載了(或者說(shuō)沒(méi)有安裝)apt倉(cāng)庫(kù)里的libprotobuf-dev;2)給find_path()傳入的搜索參數(shù)也不能讓它找到這樣的目錄。注意到FindProtobuf.cmake開(kāi)頭的多行注釋中提到,可以設(shè)置(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)
因此,可以通過(guò)指定Protobuf_INCLUDE_DIR變量,來(lái)讓find_package(Protobuf REQUIRED)正確的找到頭文件目錄(真是“多此一舉”)。
而根據(jù)前一篇對(duì)find_path()的第一條規(guī)則的了解,只要設(shè)定CMAKE_SYSTEM_PREFIX_PATH追加一個(gè)能找到google/protobuf/service.h的目錄,就可以正確的產(chǎn)生Protobuf_INCLUDE_DIR變量。
實(shí)測(cè)發(fā)現(xiàn),CMAKE_SYSTEM_PREFIX_PATH和CMAKE_PREFIX_PATH的設(shè)定,都可以影響find_path()。在本文的分析場(chǎng)景中,以下兩種設(shè)定都可以讓Protobuf_INCLUDE_PATH產(chǎn)生正確的值(但是庫(kù)文件還是找不到的,暫時(shí)忽略):
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的文檔頁(yè)面,此變量是若干其它變量取值的拼接,不建議修改;鼓勵(lì)修改CMAKE_PREFIX_PATH。
而CMAKE_PREFIX_PATH的文檔頁(yè)面,則表明了它是用來(lái)在find_package(), find_library(), find_program(), find_file(), find_path()等命令中執(zhí)行查找時(shí)提供prefix的選擇。
2. Protobuf的庫(kù)文件
本小結(jié)探究Protobuf的庫(kù)文件被搜索到的過(guò)程。我們首先確保Protobuf的頭文件搜索能被找到,這次選擇在CMAKE_PREFIX_PATH里進(jìn)行設(shè)定,對(duì)應(yīng)的輸出:
-- 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字樣,其實(shí)是protobuf庫(kù)文件沒(méi)找到的造成的。
根據(jù)前面分析,以及文檔中對(duì)CMAKE_PREFIX_PATH的說(shuō)明,我們讓CMAKE_PREFIX_PATH再增加一項(xiàng),也就是protobuf的庫(kù)文件所在目錄
list(APPEND CMAKE_PREFIX_PATH "/home/zz/soft/protobuf-3.8.0/include;/home/zz/soft/protobuf-3.8.0/lib")
清理CMakeCache.txt后重新執(zhí)行cmake,protobuf的庫(kù)文件就能被正確的找到了,find_package(Protobuf REQUIRED)因而不再報(bào)錯(cuò):
-- Found Protobuf: /home/zz/soft/protobuf-3.8.0/lib/libprotobuf.a;-lpthread (found version "3.8.0")
3. Protobuf可執(zhí)行文件
大多數(shù)用到Protobuf的C/C++工程,只需要find_protobuf(Protobuf)能提供頭文件搜索目錄、庫(kù)文件絕對(duì)路徑。
但也有那么一小撮C/C++程序,還需要調(diào)用protobuf的編譯器,也就是名為protoc的可執(zhí)行文件。對(duì)于本文討論的情況,我們并沒(méi)有假設(shè)~/soft/protobuf-3.8.0/bin放在了PATH環(huán)境變量中。你可以放,但既然已經(jīng)是手動(dòng)編譯的Protobuf了,也應(yīng)該知道不在PATH里添加protoc所在目錄的情況下,在CMakeLists.txt中進(jìn)行設(shè)定的方式。
依然是翻看FindProtobuf.cmake,發(fā)現(xiàn)可以手動(dòng)指定Protobuf_PROTOC_EXECUTABLE這一緩存變量,不過(guò)這讓人覺(jué)得多此一舉。
而在CMAKE_PREFIX_PATH的文檔頁(yè)中提到,它里面的值作為prefix可以用于find_program(),而FindProtobuf.cmake中對(duì)于protoc的查找正是基于find_program()實(shí)現(xiàn)的。因此仍然是在CMAKE_PREFIX_PATH中添加一項(xiàng),來(lái)找到protoc。然而一次性塞了include目錄、庫(kù)目錄、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}")
當(dāng)然,實(shí)際的例子可能還需要額外的設(shè)定。來(lái)看具體的例子吧。
4. Protobuf的一個(gè)實(shí)際例子
這里例子中,既需要Protobuf的include目錄、庫(kù)文件路徑,也需要protoc的可執(zhí)行路徑;而因?yàn)橛昧藀rotobuf3.8,還需要開(kāi)啟C++11。
目錄結(jié)構(gòu):
(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;
}
具體的構(gòu)建過(guò)程:
mkdir build
cd build
cmake ..
make
5. 總結(jié)
通常,使用Protobuf作為依賴(lài)庫(kù)的C/C++程序,并且Protobuf是自行編譯安裝的版本;設(shè)定CMAKE_PREFIX_PATH為同時(shí)包含protobuf的include目錄、庫(kù)目錄,然后執(zhí)行find_package(Protobuf)即可。
個(gè)別復(fù)雜的,還需要添加可執(zhí)行文件的目錄到CMAKE_PREFIX_PATH。如果還是不夠用(例如cmake正常而make階段報(bào)錯(cuò)),則翻看FindProtobuf.cmake并結(jié)合CMake官方文檔查閱即可。