由於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框架知識簡介。