深入理解CMake(5):find_package尋找手動(dòng)編譯安裝的Protobuf過(guò)程分析

package-153360_960_720.png

先前分析了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_PATHCMAKE_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;-lpthreadNOTFOUND字樣,其實(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官方文檔查閱即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容