使用CEF(七)详解macOS下基于CEF的多进程应用程序CMake项目搭建
由于macOS下的應用程序結構導致了CEF這樣的多進程架構程序在項目結構、運行架構上有很多細節需要關注,這一塊的內容比起Windows要復雜的多,所以本文將會聚焦macOS下基于CEF的多進程應用架構的環境配置,并逐一說明了CMake的相關用法和CEF應用配置細節。
前言
在進行搭建之前,我們首先必須要弄清楚一個問題,我們最終到底要生成幾個可執行應用。為什么要搞清楚這個問題呢?了解CEF的讀者都知道,CEF屬于多進程架構體系,包含有一個主進程管理整個瀏覽器應用(包括原生GUI窗體等),以及多種類型的子進程各自獨立負責各自的職責(比如渲染進程以及GPU加速進程等)。
筆者在以前的文章中曾介紹過CEF中提供的樣例cefsimple在Windows操作系統上的構建流程,我們發現這個cefsimple項目在編譯后會最終只生成了一個exe可執行程序,而在運行時為了達到多進程的目的,該exe首先作為主進程入口啟動,內部在準備啟動子進程的時候,其做法是調用該exe本身,并通過命令行參數的形式來區分主進程和其他子進程。也就是說,該exe應用內部不僅包含了主進程代碼,也包含了子進程代碼,源代碼中會根據命令行參數(--type=xxx)通過分支讓主進程和子進程走到不同的邏輯:
而在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目錄中:
項目根目錄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.cmake和cef_binary_xxx/cmake/cef_macros.cmake兩個CMake配置文件。
這兩個文件的作用分別是定義一些CEF提供的變量和宏方法,以便在后續的CMake加載邏輯中使用。
在find_package以后,我們調用了add_subdirectory指令,該指令第一個參數${CEF_LIBCEF_DLL_WRAPPER_PATH}就使用了來自cef_variables.cmake中定義值,指代了libcef_dll_wrapper代碼工程的目錄:
因此,這里的邏輯就是將cef_binary_xxx/libcef_dll目錄作為了我們的CMake子模塊工程,于是CMake會進一步加載cef_binary_xxx/libcef_dll/CMakeLists.txt文件并進行CMake相關文件的生成。細心的讀者會注意到,這里還存在第二個參數libcef_dll_wrapper:
這里需要這個參數值的原因在于,libcef_dll_wrapper所在目錄是一個外部路徑,所以需要提供一個目錄名作為的CMake文件二進制生成的路徑。如果不提供,則會收到錯誤:
那么第二個參數具體影響了什么呢?如果讀者使用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路徑的產生:
至此,我們添加了對CEF的libcef_dll_wrapper子模塊的引入,為了驗證模塊引入的正確性,我們嘗試在當前cef_app_macos_project這個項目中對引入的子模塊進行編譯。有兩種操作方式,方式1就是進入cmake-build-debug這個目錄下使用命令:cmake --build .;當然,我們還可以使用IDE提供的更加便利的方式2:CLion直接使用GUI即可。
如果一切沒有問題的情況下,我們可以在output目錄中找到libcef_dll_wrapper的生成出來的庫文件:
在繼續后面的講解前,我們先放慢腳步,對項目環境做一個總結。我們首先準備了兩個目錄,一個是我們自己的cef_app_macos_project目錄,我們會在這個項目中“引入”CEF相關庫,后續還會在里面編寫我們自己的應用程序;另一個則是在外部的cef_binary_xxx目錄,我們不會改動其中的內容。
對于我們自己的cef_app_macos_project,在根目錄下,我們編寫了一個CMakeLists.txt,它是我們項目頂層的CMake配置,該文件核心配置邏輯分以下幾步:
- 一些基本的項目、編譯配置;
- 加載CEF的CMake配置;
- 引入外部的
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。下圖是沒有配置該值前后的對比:
可以看到,沒有配置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下需要Cocoa、AppKit等框架庫。讀者可以翻閱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目錄下:
在上面的講解中我們大致理解了macOS的App Bundle的應用程序組織結構,細心的讀者會發現,在構建后的Bundle中的根目錄下有一個文件Info.plist:
該文件的核心作用是定義macOS下App Bundle的基礎應用程序配置,包括不限于該應用的名稱、應用ID、圖標資源等。因為我們將主進程target定義為了MACOS_BUNDLE,CMake會在構建的時候,默認為我們的Bundle生成了一份plist并寫入到Bundle中。同時我們會發現,Info.plist配置中關于CFBundleName、CFBundleIdentifier等值就是我們現在的target的名稱:
原因在于配置文件中緊接著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_NAME和MACOSX_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文件,具體的意義和其內容我們后面介紹,這里讀者可以將它理解為一份模板文件。
此時,我們的項目結構如下:
為了閱讀的方便,我們都將子進程叫做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的幾個知識點:
- file(READ)讀取某個文件并存放到文本變量中;
- string(REPLAECE)替換文本變量中某些字符串并寫回到變量中;
- 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_NAME、MACOSX_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,并已經成功復制到了對應目錄中:
寫在最后
在本文,我們基本上完成了在macOS下基于CEF的多進程應用架構的項目CMake配置,并結合實際的配置,逐一說明了CMake的相關用法和配置細節。在下一篇文章中,我們會基于此文搭建的項目,逐步介紹并編寫macOS下基于CEF應用程序的代碼,其中會涉及到macOS下Cocoa框架知識簡介。
本文倉庫鏈接:w4ngzhen/cef_app_macos_project (github.com)
總結
以上是生活随笔為你收集整理的使用CEF(七)详解macOS下基于CEF的多进程应用程序CMake项目搭建的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 箱包品牌推广文案29句
- 下一篇: 不知不觉发现已经有三个Wi-Fi6设备了