使用CEF(七)詳解macOS下基於CEF的多程式應用程式CMake專案搭建

w4ngzhen發表於2023-12-12

由於macOS下的應用程式結構導致了CEF這樣的多程式架構程式在專案結構、執行架構上有很多細節需要關注,這一塊的內容比起Windows要複雜的多,所以本文將會聚焦macOS下基於CEF的多程式應用架構的環境配置,並逐一說明了CMake的相關用法和CEF應用配置細節。

前言

在進行搭建之前,我們首先必須要弄清楚一個問題,我們最終到底要生成幾個可執行應用。為什麼要搞清楚這個問題呢?瞭解CEF的讀者都知道,CEF屬於多程式架構體系,包含有一個主程式管理整個瀏覽器應用(包括原生GUI窗體等),以及多種型別的子程式各自獨立負責各自的職責(比如渲染程式以及GPU加速程式等)。

筆者在以前的文章中曾介紹過CEF中提供的樣例cefsimple在Windows作業系統上的構建流程,我們發現這個cefsimple專案在編譯後會最終只生成了一個exe可執行程式,而在執行時為了達到多程式的目的,該exe首先作為主程式入口啟動,內部在準備啟動子程式的時候,其做法是呼叫該exe本身,並透過命令列引數的形式來區分主程式和其他子程式。也就是說,該exe應用內部不僅包含了主程式程式碼,也包含了子程式程式碼,原始碼中會根據命令列引數(--type=xxx)透過分支讓主程式和子程式走到不同的邏輯:

010-cef-exe-excute-flow

而在macOS下,由於macOS本身對於應用程式的許可權管理與Windows存在差異,它具備有一套特殊的沙盒機制來保證應用程式彼此獨立和安全。所以,我們不建議像Windows那樣最終透過編譯生成一個App Bundle,來多次啟動自己。一個很直觀的例子可以解釋這一點:假設我們現在基於CEF的應用程式編譯並構建了一個App Bundle,這個app內將主程式程式碼和子程式程式碼寫在了一起,透過執行時邏輯來區分。此時,假設主程式需要macOS的“鑰匙串”許可權,讀取使用者的一些配置。由於macOS許可權是給到Bundle應用層面的,所以儘管我們只想讓主程式得到“鑰匙串”訪問許可權,但因為主程式和子程式都是同一個Bundle,無形中導致了子程式也同樣擁有了這個許可權,而像渲染程式這樣的子程式,裡面會執行js程式碼、wasm等第三方程式碼邏輯,一旦出現了BUG,就會存在許可權洩漏風險。如果我們把主程式和子程式分離到兩個Bundle,主程式所在Bundle獲取某些系統許可權,而渲染程式獲取某些必要許可權,就能做到主程式和子程式許可權分離的目的,為安全性提供了一定保證。

所以,在瞭解了macOS下的CEF應用構建思路以後,我們開始搭建對應專案,並在搭建過程中對涉及的配置逐一解釋,希望能夠幫助讀者理清專案脈絡。

搭建

基礎準備

搭建的步驟分為以下幾步:

1)下載cef的二進位制分發檔案(cef_binary_xxx),將它解壓存放到某個資料夾(可以不用放在專案目錄下);

2)配置一個環境變數CEF_ROOT,需要該環境變數值配置為cef_binary_xxx所在目錄:

❯ echo $CEF_ROOT
/Users/w4ngzhen/projects/thirds/cef_binary_119.4.7+g55e15c8+chromium-119.0.6045.199_macosarm64
# 配置完成後,請確保環境變數生效

3)建立專案目錄cef_app_macos_project,該目錄將會存放本次macOS下工程的所有配置、原始碼。

4)在專案根目錄下建立cmake目錄,並將步驟1中cef_binary_xxx/cmake/FindCef.cmake檔案複製到cmake目錄中:

020-copy-FindCEF

專案根目錄CMake配置

前期工作準備好以後,我們在專案根目錄下建立CMakeLists.txt檔案,並編寫如下內容:

CMAKE_MINIMUM_REQUIRED(VERSION 3.21)

PROJECT(cef_app_macos_project LANGUAGES CXX)

# 基礎配置
SET(CMAKE_BUILD_TYPE DEBUG)
SET(CMAKE_CXX_STANDARD 17)
SET(CMAKE_CXX_STANDARD_REQUIRED ON)
SET(CMAKE_INCLUDE_CURRENT_DIR ON)

# ===== CEF =====
if (NOT DEFINED ENV{CEF_ROOT})
    message(FATAL_ERROR "環境變數CEF_ROOT未定義!")
endif ()
# 執行下面之前,請確保環境變數CEF_ROOT已經配置為了對應cef_binary_xxx目錄
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(CEF REQUIRED)

# ===== 子模組引入 =====
# 1. CEF前置準備完成後,此處便可以使用變數 CEF_LIBCEF_DLL_WRAPPER_PATH ,該值會返回libcef_dll_wrapper的目錄地址
add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)

關於CMake本身的基礎配置定義我們不再贅述,這裡主要解釋一下關於CEF引入的部分。首先,我們並沒有把cef_bin_xxx目錄複製到專案根目錄下,而是放在了“外部”,並透過環境變數CEF_ROOT指向了它。在上述CMake關於CEF配置部分,我們對CMAKE_MODULE_PATH路徑值追加了cef_app_macos_project/cmake目錄。

${CMAKE_CURRENT_SOURCE_DIR}就指代了專案根目錄cef_app_macos_project

接下來,在find_package(CEF REQUIRED)的時候,CMake會搜尋CMAKE_MODULE_PATH路徑下的名為FindCEF.cmake的CMake配置,於是就能找到我們曾複製的cef_app_macos_project/cmake/FindCEF.cmake檔案並進行載入。

如果CMake初始化的時候出現了:

CMake Error at CMakeLists.txt:20 (message):
環境變數CEF_ROOT未定義!

請確保CEF_ROOT環境變數確定配置了。

對於FindCEF.cmake本身的內容,其核心邏輯就是讀取環境變數CEF_ROOT值,然後定位到cef_binary_xxx目錄,並載入cef_binary_xxx/cmake/cef_variables.cmakecef_binary_xxx/cmake/cef_macros.cmake兩個CMake配置檔案。

這兩個檔案的作用分別是定義一些CEF提供的變數和宏方法,以便在後續的CMake載入邏輯中使用。

find_package以後,我們呼叫了add_subdirectory指令,該指令第一個引數${CEF_LIBCEF_DLL_WRAPPER_PATH}就使用了來自cef_variables.cmake中定義值,指代了libcef_dll_wrapper程式碼工程的目錄:

030-libcef_dll_wrapper_var_path

因此,這裡的邏輯就是將cef_binary_xxx/libcef_dll目錄作為了我們的CMake子模組工程,於是CMake會進一步載入cef_binary_xxx/libcef_dll/CMakeLists.txt檔案並進行CMake相關檔案的生成。細心的讀者會注意到,這裡還存在第二個引數libcef_dll_wrapper

040-pin-add_subdir_param

這裡需要這個引數值的原因在於,libcef_dll_wrapper所在目錄是一個外部路徑,所以需要提供一個目錄名作為的CMake檔案二進位制生成的路徑。如果不提供,則會收到錯誤:

050-add_subdir_no-param-error

那麼第二個引數具體影響了什麼呢?如果讀者使用CLion+CMake,會看到CLion會在專案根目錄下生成cmake-build-debug目錄,這個就是CMake生成檔案目錄,編譯後的結果、CMake的過程檔案都會在這個目錄下找到(該目錄其實就是cmake命令列的-B引數指定的路徑,CLion預設指定的專案根目錄下/cmake-build-debug目錄)。在這裡,當我們add_subdirectory新增了libcef_dll_wrapper子模組,經過CMake的初始化以後,會看到cmake-build-debug/libcef_wrapper_dll路徑的產生:

060-cmake-bin-dir-generate.png

至此,我們新增了對CEF的libcef_dll_wrapper子模組的引入,為了驗證模組引入的正確性,我們嘗試在當前cef_app_macos_project這個專案中對引入的子模組進行編譯。有兩種操作方式,方式1就是進入cmake-build-debug這個目錄下使用命令:cmake --build .;當然,我們還可以使用IDE提供的更加便利的方式2:CLion直接使用GUI即可。

070-build-dll-wrapper

如果一切沒有問題的情況下,我們可以在output目錄中找到libcef_dll_wrapper的生成出來的庫檔案:

080-libcef_dll_wrapper-build-ok

在繼續後面的講解前,我們先放慢腳步,對專案環境做一個總結。我們首先準備了兩個目錄,一個是我們自己的cef_app_macos_project目錄,我們會在這個專案中“引入”CEF相關庫,後續還會在裡面編寫我們自己的應用程式;另一個則是在外部的cef_binary_xxx目錄,我們不會改動其中的內容。

對於我們自己的cef_app_macos_project,在根目錄下,我們編寫了一個CMakeLists.txt,它是我們專案頂層的CMake配置,該檔案核心配置邏輯分以下幾步:

  1. 一些基本的專案、編譯配置;
  2. 載入CEF的CMake配置;
  3. 引入外部的cef_binary_xxx中的libcef_dll_wrapper模組作為CMake子模組。

但請注意,目前我們僅僅是透過CMake提供的add_subdirectory命令,將libcef_dll_wrapper作為子模組引入,但目前還沒有任何的應用在依賴它,接下來我們將進一步,開始配置主程式應用,並依賴該libcef_dll_wrapper

主程式應用專案配置

在專案根目錄下,我們建立cef_app目錄,該目錄目前先存放CEF的macOS應用的主程式應用專案程式碼。我們在cef_app目錄下建立process_main.mm,且暫時先編寫一段簡單的程式碼:

#include <iostream>

int main(int argc, char *argv[]) {
  std::cout << "hello, this is main process." << std::endl;
  return 0;
}

PS:.mm為字尾檔案是指Objective-C與C/C++混寫的原始碼檔案字尾,所以這裡我們是可以完全寫C++程式碼的。

然後,在cef_app目錄中建立CMakeLists.txt檔案,並編寫如下的配置:

# ===== 主程式target配置 =====
# 主程式target名稱
set(CEF_APP_TARGET cef_app)
# 最終 App Bundle生成的路徑
set(CEF_APP_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/${CEF_APP_TARGET}.app")
# 新增專案所有的原始檔:
add_executable(
        ${CEF_APP_TARGET}
        MACOSX_BUNDLE # macOS 使用 "MACOSX_BUNDLE" 標識,最後編譯產物是一個mac下的App Bundle
        process_main.mm
)
# 使用CEF提供的預定義好的工具宏,該宏會幫助配置target一些編譯上的配置
# 如果出現不符合預期的編譯結果、執行錯誤,可以檢查該宏的內部實現
SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_APP_TARGET})
# 新增對 libcef_dll_wrapper 庫的依賴
# 基於該配置,可以保證每次編譯當前 cef_app target時候,確保 libcef_dll_wrapper 靜態庫編譯完成
add_dependencies(${CEF_APP_TARGET} libcef_dll_wrapper)
# 連結庫配置
target_link_libraries(
        ${CEF_APP_TARGET}
        PRIVATE
        # libcef_dll_wrapper庫連結
        libcef_dll_wrapper
        # 該變數來自cef_variables.cmake中定義的配置
        # 主要是針對不同的平臺,連結對應平臺的一些標準庫(Windows、Linux)或者framework(macOS)
        ${CEF_STANDARD_LIBS}
)
# 主程式編譯後,會在輸出目錄下生成一個名為 cef_app.app 的macOS App Bundle。
# 該app內部 Contents/MacOS/cef_app 僅僅是包含了 add_executable 中的原始碼二進位制,以及libcef_dll_wrapper靜態庫
# 在macOS下,我們還需要將"cef_binary_xxx/Debug或Release目錄/Chromium Embedded Framework.framework"複製到
# cef_app.app/Contents/Frameworks目錄下
# 為了避免手動複製的麻煩,我們使用如下的指令完成複製工作
add_custom_command(
        # 對 CEF_APP_TARGET 進行操作
        TARGET ${CEF_APP_TARGET}
        # 在構建完成後(POST_BUILD)
        POST_BUILD
        # COMMAND ${CMAKE_COMMAND}:就是命令列執行 "cmake"
        # -E:指可以執行一些cmake內建的工具命令
        # copy_directory:進行目錄複製操作
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        # 複製源目錄、檔案,
        # CEF_BINARY_DIR變數來源於cef_variables.cmake
        # 等價於"cef_binary_xxx目錄/Debug或Release目錄/"
        "${CEF_BINARY_DIR}/Chromium Embedded Framework.framework"
        # 將上述 framework 複製到 當前生成的 cef_app.app/Contents/Frameworks/對應framework名稱
        "${CEF_APP_BUNDLE}/Contents/Frameworks/Chromium Embedded Framework.framework"
        # 不進行文字的解析,使用源文字,考慮會有表示式情況
        VERBATIM
)
# 簡單配置Info.plist的一些值
set_target_properties(
        ${CEF_APP_TARGET}
        PROPERTIES
        MACOSX_BUNDLE_BUNDLE_NAME ${CEF_APP_TARGET}
        MACOSX_BUNDLE_GUI_IDENTIFIER ${CEF_APP_TARGET}
)

我們接下來對上述的配置逐一解釋:

# 主程式target名稱
set(CEF_APP_TARGET cef_app)
# 最終 App Bundle生成的路徑
set(CEF_APP_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/${CEF_APP_TARGET}.app")

上述配置了我們接下來將會定義的target的名稱,以及後續生成的macOS特有的App Bundle的應用檔案的路徑,後續會使用到該值。

add_executable(
        ${CEF_APP_TARGET}
        MACOSX_BUNDLE # macOS 使用 "MACOSX_BUNDLE" 標識,最後編譯產物是一個mac下的App Bundle
        process_main.mm
)

add_executable部分定義最終生成的target,除了包含編寫的原始碼路徑(process_main.mm),這裡還有一個很重要的引數MACOS_BUNDLE,配置該引數後,在macOS下,我們最終生成的可執行程式就不再是一個簡單的命令列程式,而是macOS下的App Bundle。下圖是沒有配置該值前後的對比:

090-MACOS_BUNDLE-param-diff

可以看到,沒有配置MACOSX_BUNDLE時,最終專案會在輸出目錄(${CMAKE_CURRENT_BINARY_DIR})下生成名為cef_app的可執行命令列程式;而配置以後,專案會在輸出目錄下生成target名.app,這裡就是cef_app.app

# 使用CEF提供的預定義好的工具宏,該宏會幫助配置target一些編譯上的配置
# 如果出現不符合預期的編譯結果、執行錯誤,可以檢查該宏的內部實現
SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_APP_TARGET})

SET_EXECUTABLE_TARGET_PROPERTIES不是CMake提供的指令,而是由CEF提供的,存放於cef_macros.cmake中的宏。該宏主要的功能是對目標target配置一些可執行程式所需要的編譯引數等。如果讀者在實踐過程中,遇到了連結問題,可以優先檢查這個宏中的實現。由於篇幅原因,這塊後續單獨出一篇文章水一水,>_<。

# 新增對 libcef_dll_wrapper 庫的依賴
# 基於該配置,可以保證每次編譯當前 cef_app target時候,確保 libcef_dll_wrapper 靜態庫編譯完成
add_dependencies(${CEF_APP_TARGET} libcef_dll_wrapper)

add_dependencies的作用則是為當前target指定依賴。因為我們的專案本身會透過靜態連結庫的形式連結libcef_dll_wrapper,透過這add_dependencies能夠保證最終構建過程中,確保優先將libcef_dll_wrapper編譯出來,供後續連結過程使用。當然,你也可以不閒麻煩的手動先編譯libcef_dll_wrapper,再編譯這個cef_app

# 連結庫配置
target_link_libraries(
        ${CEF_APP_TARGET}
        PRIVATE
        # libcef_dll_wrapper庫連結
        libcef_dll_wrapper
        # 該變數來自cef_variables.cmake中定義的配置
        # 主要是針對不同的平臺,連結對應平臺的一些標準庫(Windows、Linux)或者framework(macOS)
        ${CEF_STANDARD_LIBS}
)

target_link_libraries處理則是配置當前target的連結庫,包括不限於libcef_dll_wrapper的靜態連結、各種平臺特定的連結庫等。最後一個引數變數CEF_STANDARD_LIBS,由CEF在cef_variables.cmake中定義,包含平臺特定的連結庫。

例如,在Windows下我們可能需要gdi32.lib,在Linux構建窗體可能需要X11庫,以及在macOS下需要CocoaAppKit等框架庫。讀者可以翻閱cef_variables.cmake中關於這個變數的配置瞭解具體的內容。

# 主程式編譯後,會在輸出目錄下生成一個名為 cef_app.app 的macOS App Bundle。
# 該app內部 Contents/MacOS/cef_app 僅僅是包含了 add_executable 中的原始碼二進位制,以及libcef_dll_wrapper靜態庫
# 在macOS下,我們還需要將"cef_binary_xxx/Debug或Release目錄/Chromium Embedded Framework.framework"複製到
# cef_app.app/Contents/Frameworks目錄下
# 為了避免手動複製的麻煩,我們使用如下的指令完成複製工作
add_custom_command(
        # 對 CEF_APP_TARGET 進行操作
        TARGET ${CEF_APP_TARGET}
        # 在構建完成後(POST_BUILD)
        POST_BUILD
        # COMMAND ${CMAKE_COMMAND}:就是命令列執行 "cmake"
        # -E:指可以執行一些cmake內建的工具命令
        # copy_directory:進行目錄複製操作
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        # 複製源目錄、檔案,
        # CEF_BINARY_DIR變數來源於cef_variables.cmake
        # 等價於"cef_binary_xxx目錄/Debug或Release目錄/"
        "${CEF_BINARY_DIR}/Chromium Embedded Framework.framework"
        # 將上述 framework 複製到 當前生成的 cef_app.app/Contents/Frameworks/對應framework名稱
        "${CEF_APP_BUNDLE}/Contents/Frameworks/Chromium Embedded Framework.framework"
        # 不進行文字的解析,使用源文字,考慮會有表示式情況
        VERBATIM
)

倒數第二個指令add_custom_command,在介紹它的作用前,先簡單說明在macOS下基於CEF的App Bundle的一應用結構。基於前面的配置,主程式編譯後,會在輸出目錄下生成一個名為cef_app.app的macOS App Bundle,該Bundle內部/Contents/MacOS/cef_app可執行程式,就是連結了原始碼二進位制、libcef_dll_wrapper靜態庫後的可執行二進位制程式。然而,CEF核心庫Chromium Embedded Framework.framework我們並沒有靜態連結到執行程式內,而是在實際執行過程中,動態載入這個framework。為了達到該目的,我們思路是透過指令碼將cef_binary_xxx中提供的CEF的核心庫framework複製到App Bundle中指定路徑下。

所以,在瞭解了App Bundle執行邏輯以後,關於add_custom_command作用就顯而易見了,其邏輯就是配置在構建完成以後,透過CMake的工具指令(-E copy_directories)將Chromium Embedded Framework.framework整個內容複製到生成的Bundle的/Contents/Frameworks目錄下:

100-copy-CEF-framework

在上面的講解中我們大致理解了macOS的App Bundle的應用程式組織結構,細心的讀者會發現,在構建後的Bundle中的根目錄下有一個檔案Info.plist

110-info-plist-file

該檔案的核心作用是定義macOS下App Bundle的基礎應用程式配置,包括不限於該應用的名稱、應用ID、圖示資源等。因為我們將主程式target定義為了MACOS_BUNDLE,CMake會在構建的時候,預設為我們的Bundle生成了一份plist並寫入到Bundle中。同時我們會發現,Info.plist配置中關於CFBundleNameCFBundleIdentifier等值就是我們現在的target的名稱:

120-info-plist-content

原因在於配置檔案中緊接著add_custom_command後面的set_target_properties

# 簡單配置Info.plist的一些值
set_target_properties(
        ${CEF_APP_TARGET}
        PROPERTIES
        MACOSX_BUNDLE_BUNDLE_NAME ${CEF_APP_TARGET}
        MACOSX_BUNDLE_GUI_IDENTIFIER ${CEF_APP_TARGET}
)

使用set_target_properties指令指定了MACOSX_BUNDLE_BUNDLE_NAMEMACOSX_BUNDLE_GUI_IDENTIFIER的值。關於這段配置的說明,官方文件提到:https://cmake.org/cmake/help/latest/prop_tgt/MACOSX_BUNDLE_INFO_PLIST.html,我們可以直接透過相關屬性值來替換CMake內建的plist模板檔案內容。

注意,CMake支援的變數只有上述官方文件提供的Key,如果有其他的Key需要處理,只能透過自己提供模板方法進行處理,這點會在後面構建子程式Bundle再次說明。

至此,我們基本完成了在macOS對主程式的CMake配置。此時,請務必注意,記得在專案根目錄的CMakeLists.txt追加如下將cef_app目錄作為子模組引入的配置:

# 1. CEF前置準備完成後,此處便可以使用變數 CEF_LIBCEF_DLL_WRAPPER_PATH ,該值會返回libcef_dll_wrapper的目錄地址
add_subdirectory(${CEF_LIBCEF_DLL_WRAPPER_PATH} libcef_dll_wrapper)
+ # 2. 將cef_app作為子模組引入
+ add_subdirectory(./cef_app)

當然,我們主程式應用的原始碼還是隻是簡單的在控制檯輸出一段話,我們不著急編寫主程式程式碼,接下來還需要配置對應的子程式專案。

子程式應用專案配置

我們在一開始已經提到過,在macOS建議將主程式和子程式分別構建為兩個不同的App Bundle,這裡我們有兩種做法:

  • 方式1:透過CMake的定義target,在前面主程式CMakeLists.txt中直接定義子程式的target,讓構建系統同時生成另外的子程式應用。

  • 方式2:直接重新建立一個目錄來定義子程式CMake模組並存放子程式模組程式碼。

這裡筆者使用第一種方式來進行配置,或許配置上略顯複雜,但只要讀者一旦理解,筆者相信今後對於其他CMake專案配置應該也能很快上手。

我們先在cef_app目錄中建立一個名為process_helper.mm的檔案,暫時作為子程式的入口原始碼:

#include <iostream>

int main(int argc, char *argv[]) {
  std::cout << "hello, this is sub helper process." << std::endl;
  return 0;
}

同時,在該子模組目錄下建立一個templates目錄,並在其中建立helper-Info.plist檔案,具體的意義和其內容我們後面介紹,這裡讀者可以將它理解為一份模板檔案。

此時,我們的專案結構如下:

130-sub-process-new-file

為了閱讀的方便,我們都將子程式叫做helper

接下來,我們在cef_app/CMakeLists.txt內容的基礎上,新增如下的針對helper子程式應用的配置:

# ===== 主程式target配置 =====
# ... ...
# ===== 子程式 helper target配置 =====
# 定義helper子程式target名
set(CEF_APP_HELPER_TARGET "cef_app_helper")
# 定義helper子程式構建後的app的名稱
set(CEF_APP_HELPER_OUTPUT_NAME "cef_app Helper")
# 注意,上述的名稱都不是最終名稱,它們更準確的意義是作為下面迴圈定義target的基礎名稱
# 後續迴圈的時候,會基於上述名稱進行拼接

# 建立多個不同型別helper的target
# CEF_HELPER_APP_SUFFIXES來自cef_variables.cmake,是一個“字串陣列”,值有:
# "::"、" (Alerts):_alerts:.alerts"、" (GPU):_gpu:.gpu"、
# " (Plugin):_plugin:.plugin"、" (Renderer):_renderer:.renderer"
# 這裡透過foreach,實現對字串陣列的遍歷,每一次迴圈會得到一個字串,存放在“_suffix_list”
foreach (_suffix_list ${CEF_HELPER_APP_SUFFIXES})
  # 將字串轉為";"分割,這樣可以使用CMake支援的list(GET)指令來讀取每一節字串
  # 以 " (Renderer):_renderer:.renderer" 為例
  string(REPLACE ":" ";" _suffix_list ${_suffix_list}) # " (Renderer);_renderer;.renderer"
  list(GET _suffix_list 0 _name_suffix) # " (Renderer)"
  list(GET _suffix_list 1 _target_suffix) # "_renderer"
  list(GET _suffix_list 2 _plist_suffix) # ".renderer"
  # 當然,需要注意 CEF_HELPER_APP_SUFFIXES 中有一個"::"的字串,
  # 會使得 _name_suffix = ""、_target_suffix = ""、_plist_suffix = ""

  # 定義一個Helper target以及BUNDLE名稱
  # 以 " (Renderer):_renderer:.renderer" 為例
  # _helper_target = "cef_app_helper" + "_renderer" -> "cef_app_helper_renderer"
  # _helper_output_name = "cef_app Helper" + " (Renderer)" -> "cef_app Helper (Renderer)"
  set(_helper_target "${CEF_APP_HELPER_TARGET}${_target_suffix}")
  set(_helper_output_name "${CEF_APP_HELPER_OUTPUT_NAME}${_name_suffix}")

  # 讀取templates/helper-Info.plist模板檔案內容到_plist_contents
  # 然後使用上面得到的 _helper_output_name、_plist_suffix等變數進行文字內容的替換操作
  # 以便得到當前正在處理的helper對應的一份Info.plist
  file(READ "${CMAKE_CURRENT_SOURCE_DIR}/templates/helper-Info.plist" _plist_contents)
  string(REPLACE "\${HELPER_EXECUTABLE_NAME}" "${_helper_output_name}" _plist_contents ${_plist_contents})
  string(REPLACE "\${PRODUCT_NAME}" "${_helper_output_name}" _plist_contents ${_plist_contents})
  string(REPLACE "\${BUNDLE_ID_SUFFIX}" "${_plist_suffix}" _plist_contents ${_plist_contents})
  # helper的Info.plist檔案路徑,例如:"${CMAKE_CURRENT_BINARY_DIR}/helper-Info[_renderer].plist"
  set(_helper_info_plist_file "${CMAKE_CURRENT_BINARY_DIR}/helper-Info${_target_suffix}.plist")
  # 透過CMake提供file(WRITE)命令,將前面定義的內容寫入到對應.plist檔案中
  file(WRITE ${_helper_info_plist_file} ${_plist_contents})

  # 建立當前helper的executable target,當然,也是一個App Bundle
  add_executable(${_helper_target}
      MACOSX_BUNDLE
      process_helper.mm
  )
  # 與主程式應用一樣,
  # 透過cef提供的SET_EXECUTABLE_TARGET_PROPERTIES宏,來設定編譯引數、標頭檔案路徑等
  SET_EXECUTABLE_TARGET_PROPERTIES(${_helper_target})
  # 編譯當前Helper target前,先編譯 libcef_dll_wrapper target
  add_dependencies(${_helper_target} libcef_dll_wrapper)
  # 當前Helper target的庫連結
  target_link_libraries(${_helper_target} libcef_dll_wrapper ${CEF_STANDARD_LIBS})
  # 定義當前Helper target的一些屬性
  set_target_properties(${_helper_target} PROPERTIES
      # 這裡使用“MACOSX_BUNDLE_INFO_PLIST”,
      # 來定義構建過程Bundle使用的Info.plist來源於前面我們透過模板檔案生成的.plist
      MACOSX_BUNDLE_INFO_PLIST ${_helper_info_plist_file}
      # 定義最終生成的App Bundle的名稱
      OUTPUT_NAME ${_helper_output_name}
  )

  # 構建主程式應用前,會先構建當前Helper target
  add_dependencies(${CEF_APP_TARGET} "${_helper_target}")

  # 將構建的Helper App Bundle複製到主程式cef_app的Bundle中
  add_custom_command(
      TARGET ${CEF_APP_TARGET}
      POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy_directory
      "${CMAKE_CURRENT_BINARY_DIR}/${_helper_output_name}.app"
      "${CEF_APP_BUNDLE}/Contents/Frameworks/${_helper_output_name}.app"
      VERBATIM
  )
endforeach ()

讓我們從頭到尾一一道來。

# 定義helper子程式target名
set(CEF_APP_HELPER_TARGET "cef_app_helper")
# 定義helper子程式構建後的app的名稱
set(CEF_APP_HELPER_OUTPUT_NAME "cef_app Helper")
# 注意,上述的名稱都不是最終名稱,它們更準確的意義是作為下面迴圈定義target的基礎名稱
# 後續迴圈的時候,會基於上述名稱進行拼接

首先,我們會定義helper子程式的target名稱和輸出應用名稱。但需要注意的是,這裡的名稱不完全是最終輸出的應用程式的名稱。因為在後續的配置中,我們會使用CMake支援的迴圈命令來支援生成多個target。

# 建立多個不同型別helper的target
# CEF_HELPER_APP_SUFFIXES來自cef_variables.cmake,是一個“字串陣列”,值有:
# "::"、" (Alerts):_alerts:.alerts"、" (GPU):_gpu:.gpu"、
# " (Plugin):_plugin:.plugin"、" (Renderer):_renderer:.renderer"
# 這裡透過foreach,實現對字串陣列的遍歷,每一次迴圈會得到一個字串,存放在“_suffix_list”
foreach (_suffix_list ${CEF_HELPER_APP_SUFFIXES})
 ... ...
endforeach ()

接著,我們使用CMake的foreach指令,來遍歷變數CEF_HELPER_APP_SUFFIXES這個變數值。這個變數來自於cef提供的變數(cef_variables.cmake):

  # CEF Helper app suffixes.
  # Format is "<name suffix>:<target suffix>:<plist suffix>".
  set(CEF_HELPER_APP_SUFFIXES
    "::"
    " (Alerts):_alerts:.alerts"
    " (GPU):_gpu:.gpu"
    " (Plugin):_plugin:.plugin"
    " (Renderer):_renderer:.renderer"
    )

在這裡透過CMake的遍歷能力,我們每一次迭代都能讀取到對應一條字串並存放到_suffix_list變數中。

接下來介紹在foreach包裹的內部配置:

    # 將字串轉為";"分割,這樣可以使用CMake支援的list(GET)指令來讀取每一節字串
    # 以 " (Renderer):_renderer:.renderer" 為例
    string(REPLACE ":" ";" _suffix_list ${_suffix_list}) # " (Renderer);_renderer;.renderer"
    list(GET _suffix_list 0 _name_suffix) # " (Renderer)"
    list(GET _suffix_list 1 _target_suffix) # "_renderer"
    list(GET _suffix_list 2 _plist_suffix) # ".renderer"
    # 當然,需要注意 CEF_HELPER_APP_SUFFIXES 中有一個"::"的字串,
    # 會使得 _name_suffix = ""、_target_suffix = ""、_plist_suffix = ""

我們將_suffix_list變數中所有的:字元替換為;,然後就可以使用CMake支援的list(GET)指令來讀取每一節字串。

" (Renderer):_renderer:.renderer"為例,在替換後,透過list(GET)可以分別得到:

  • _name_suffix = " (Renderer)"
  • _target_suffix = "_renderer"
  • _plist_suffix = ".renderer"

這三個suffix將在後續的流程拼接出相關名稱變數。但需要注意的是,在CEF_HELPER_APP_SUFFIXES中存在一個特殊的字串:"::"。這個字串會導致最後提取出來的前面三個suffix都是""(空字串),這並不是BUG,後續會用到。

    # 定義一個Helper target以及BUNDLE名稱
    # 以 " (Renderer):_renderer:.renderer" 為例
    # _helper_target = "cef_app_helper" + "_renderer" -> "cef_app_helper_renderer"
    # _helper_output_name = "cef_app Helper" + " (Renderer)" -> "cef_app Helper (Renderer)"
    set(_helper_target "${CEF_APP_HELPER_TARGET}${_target_suffix}")
    set(_helper_output_name "${CEF_APP_HELPER_OUTPUT_NAME}${_name_suffix}")

接下來,我們開始消費suffix。首先,我們透過拼接操作得到_helper_target_helper_output_name。這兩個變數分別代表了當前正在構建的helper的真正target名和對應後續構建的應用名稱。還是以 " (Renderer):_renderer:.renderer"為例。我們能夠得到:

  • _helper_target = "cef_app_helper" + "_renderer" 得到 "cef_app_helper_renderer"
  • _helper_output_name = "cef_app Helper" + " (Renderer)" 得到 "cef_app Helper (Renderer)"
    # 讀取templates/helper-Info.plist模板檔案內容到_plist_contents
    # 然後使用上面得到的 _helper_output_name、_plist_suffix等變數進行文字內容的替換操作
    # 以便得到當前正在處理的helper對應的一份Info.plist
    file(READ "${CMAKE_CURRENT_SOURCE_DIR}/templates/helper-Info.plist" _plist_contents)
    string(REPLACE "\${HELPER_EXECUTABLE_NAME}" "${_helper_output_name}" _plist_contents ${_plist_contents})
    string(REPLACE "\${PRODUCT_NAME}" "${_helper_output_name}" _plist_contents ${_plist_contents})
    string(REPLACE "\${BUNDLE_ID_SUFFIX}" "${_plist_suffix}" _plist_contents ${_plist_contents})
    # helper的Info.plist檔案路徑,例如:"${CMAKE_CURRENT_BINARY_DIR}/helper-Info[_renderer].plist"
    set(_helper_info_plist_file "${CMAKE_CURRENT_BINARY_DIR}/helper-Info${_target_suffix}.plist")
    # 透過CMake提供file(WRITE)命令,將前面定義的內容寫入到對應.plist檔案中
    file(WRITE ${_helper_info_plist_file} ${_plist_contents})

接下來,我們使用CMake提供的能力,讀取了前面提到的存放在cef_app/templates目錄下的helper-Info.plist檔案。這是一個模板檔案,開啟後讀者能從中看到一些${XXX}的佔位字串,我們會在這一步進行對應文字的替換。這裡我們用到了CMake的幾個知識點:

  1. file(READ)讀取某個檔案並存放到文字變數中;
  2. string(REPLAECE)替換文字變數中某些字串並寫回到變數中;
  3. file(WRITE)將文字資料寫入到某個檔案中。

這一步我們還得到了_helper_info_plist_file變數,它指向了我們寫入的plist檔案,以便在後續配置中進行使用。

    # 建立當前helper的executable target,當然,也是一個App Bundle
    add_executable(${_helper_target}
            MACOSX_BUNDLE
            process_helper.mm
    )
    # 與主程式應用一樣,
    # 透過cef提供的SET_EXECUTABLE_TARGET_PROPERTIES宏,來設定編譯引數、標頭檔案路徑等
    SET_EXECUTABLE_TARGET_PROPERTIES(${_helper_target})
    # 編譯當前Helper target前,先編譯 libcef_dll_wrapper target
    add_dependencies(${_helper_target} libcef_dll_wrapper)
    # 當前Helper target的庫連結
    target_link_libraries(${_helper_target} libcef_dll_wrapper ${CEF_STANDARD_LIBS})
    # 定義當前Helper target的一些屬性
    set_target_properties(${_helper_target} PROPERTIES
            # 這裡使用“MACOSX_BUNDLE_INFO_PLIST”,
            # 來定義構建過程Bundle使用的Info.plist來源於前面我們透過模板檔案生成的.plist
            MACOSX_BUNDLE_INFO_PLIST ${_helper_info_plist_file}
            # 定義最終生成的App Bundle的名稱
            OUTPUT_NAME ${_helper_output_name}
    )

和前面主程式應用target類似。我們將helper的構建結果同樣定義為App Bundle;使用SET_EXECUTABLE_TARGET_PROPERTIES來進行編譯引數等設定;使用add_dependencies告訴CMake編譯構建子程式target的時候,保證libcef_dll_wrapper優先於helper構建完成;使用target_link_libraries連結子程式Helper。但,最後一個set_target_properties和之前主程式target設定有所不同。在之前的主程式應用配置時,我們直接使用了諸如MACOSX_BUNDLE_BUNDLE_NAMEMACOSX_BUNDLE_GUI_IDENTIFIER等引數來讓CMake使用內建的plist模板檔案生成主程式應用App Bundle中的plist檔案。但因為CMake內建的模板plist只能設定部分欄位值,而在Helper配置的時候,我們需要更改更多的佔位欄位,所以我們自己提供了helper Bundle的模板plist,並透過內容讀取、字串替換的方式生成了對應Helper的Bundle的plist檔案內容。要讓CMake不再使用內建的模板plist,而是使用我們生成的plist檔案,我們使用引數MACOSX_BUNDLE_INFO_PLIST指定前面生成好的plist檔案路徑。最後,我們還定義了OUTPUT_NAME這個引數,這個引數主要的作用是可以自定義生成的應用程式的名稱,如果沒有這個引數,我們最終在構建結果目錄中生成應用名稱就是target。

    # 構建主程式應用前,會先構建當前Helper target
    add_dependencies(${CEF_APP_TARGET} "${_helper_target}")

告訴CMake,構建主程式target應用的時候,會先構建當前Helper target。

    # 將構建的Helper App Bundle複製到主程式cef_app的Bundle中
    add_custom_command(
            TARGET ${CEF_APP_TARGET}
            POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CMAKE_CURRENT_BINARY_DIR}/${_helper_output_name}.app"
            "${CEF_APP_BUNDLE}/Contents/Frameworks/${_helper_output_name}.app"
            VERBATIM
    )

在迴圈的最後,我們再次使用add_custom_command透過CMake提供的檔案複製能力,讓主程式應用構建完成以後,將當前子程式helper應用app複製到主程式應用.app/Contents/Frameworks目錄下。至於為什麼要這麼做,我們將會在下一篇文章中介紹應用程式執行時架構來說明。

基於現在完成的配置,我們可以透過對cef_app進行構建,檢查最終構建的產物來驗證專案的正確性。筆者使用CLion的GUI生成cef_app,最終會在輸出目錄中找到cef_app.app,同時會看到會生成多個helper的App Bundle,並已經成功複製到了對應目錄中:

140-build-result

寫在最後

在本文,我們基本上完成了在macOS下基於CEF的多程式應用架構的專案CMake配置,並結合實際的配置,逐一說明了CMake的相關用法和配置細節。在下一篇文章中,我們會基於此文搭建的專案,逐步介紹並編寫macOS下基於CEF應用程式的程式碼,其中會涉及到macOS下Cocoa框架知識簡介。

本文倉庫連結:w4ngzhen/cef_app_macos_project (github.com)

相關文章