使用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所在目录:

powershell 复制代码
❯ 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 复制代码
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代码工程的目录:

因此,这里的逻辑就是将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配置,该文件核心配置逻辑分以下几步:

  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,且暂时先编写一段简单的代码:

objc 复制代码
#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文件,并编写如下的配置:

cmake 复制代码
# ===== 主进程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}
)

我们接下来对上述的配置逐一解释:

cmake 复制代码
# 主进程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的应用文件的路径,后续会使用到该值。

cmake 复制代码
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

cmake 复制代码
# 使用CEF提供的预定义好的工具宏,该宏会帮助配置target一些编译上的配置
# 如果出现不符合预期的编译结果、运行错误,可以检查该宏的内部实现
SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_APP_TARGET})

SET_EXECUTABLE_TARGET_PROPERTIES不是CMake提供的指令,而是由CEF提供的 ,存放于cef_macros.cmake中的宏。该宏主要的功能是对目标target配置一些可执行程序所需要的编译参数等。如果读者在实践过程中,遇到了链接问题,可以优先检查这个宏中的实现。由于篇幅原因,这块后续单独出一篇文章水一水,>_<。

cmake 复制代码
# 添加对 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

cmake 复制代码
# 链接库配置
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中关于这个变量的配置了解具体的内容。

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配置中关于CFBundleNameCFBundleIdentifier等值就是我们现在的target的名称:

原因在于配置文件中紧接着add_custom_command后面的set_target_properties

cmake 复制代码
# 简单配置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的值。关于这段配置的说明,官方文档提到:cmake.org/cmake/help/...

注意,CMake支持的变量只有上述官方文档提供的Key,如果有其他的Key需要处理,只能通过自己提供模板方法进行处理,这点会在后面构建子进程Bundle再次说明。

至此,我们基本完成了在macOS对主进程的CMake配置。此时,请务必注意,记得在项目根目录的CMakeLists.txt追加如下将cef_app目录作为子模块引入的配置:

diff 复制代码
# 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的文件,暂时作为子进程的入口源码:

objc 复制代码
#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子进程应用的配置:

cmake 复制代码
# ===== 主进程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 ()

让我们从头到尾一一道来。

cmake 复制代码
# 定义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。

cmake 复制代码
# 创建多个不同类型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):

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 复制代码
    # 将字符串转为";"分割,这样可以使用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,后续会用到。

cmake 复制代码
    # 定义一个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)"
cmake 复制代码
    # 读取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文件,以便在后续配置中进行使用。

cmake 复制代码
    # 创建当前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。

cmake 复制代码
    # 构建主进程应用前,会先构建当前Helper target
    add_dependencies(${CEF_APP_TARGET} "${_helper_target}")

告诉CMake,构建主进程target应用的时候,会先构建当前Helper target。

cmake 复制代码
    # 将构建的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)

相关推荐
Wect3 天前
浏览器缓存机制
前端·面试·浏览器
FliPPeDround7 天前
浏览器扩展 E2E 测试的救星:vitest-environment-web-ext 让你告别繁琐配置
e2e·浏览器·测试
SuperEugene7 天前
浏览器存储:localStorage / sessionStorage / cookie 应该怎么用
前端·javascript·面试·浏览器
宁雨桥7 天前
浏览器渲染原理
前端·浏览器·原理
YZ0999 天前
2026年如何批量保存小红书作者主页的视频、图片和文案?
经验分享·浏览器·插件
程序员ys9 天前
网页白屏的原理与优化
前端·性能优化·浏览器
Wect11 天前
从输入URL到页面显示的完整技术流程
前端·面试·浏览器
NEXT0611 天前
从输入 URL 到页面展示的完整链路解析
网络协议·面试·浏览器
CappuccinoRose14 天前
CSS 语法学习文档(十五)
前端·学习·重构·渲染·浏览器
REDcker15 天前
Media Source Extensions (MSE) 详解
前端·网络·chrome·浏览器·web·js