在chromium项目中添加新的helper app

为了创建一个生命周期独立于主进程的进程,在OS X系统上,我们需要创建一个helper app来实现这一点。在使用Xcode开发时,我们可以很轻松的创建一个子app,但是对于使用gn构建的chromium项目,这一步就显得有些麻烦了。

查看gn的官方文档,它告诉我们可以利用bundle_datacreate_bundle来创建一个的应用程序包

下面是gn的文档中提供的一个关于怎样使用bundle_data创建一个IOS/MacOS应用的例子:

  # Defines a template to create an application. On most platform, this is just
  # an alias for an "executable" target, but on iOS/macOS, it builds an
  # application bundle.
  template("app") {
    if (!is_ios && !is_mac) {
      executable(target_name) {
        forward_variables_from(invoker, "*")
      }
    } else {
      app_name = target_name
      gen_path = target_gen_dir
      # 
      action("${app_name}_generate_info_plist") {
        script = [ "//build/ios/ios_gen_plist.py" ]
        sources = [ "templates/Info.plist" ]
        outputs = [ "$gen_path/Info.plist" ]
        args = rebase_path(sources, root_build_dir) +
               rebase_path(outputs, root_build_dir)
      }

      bundle_data("${app_name}_bundle_info_plist") {
        deps = [ ":${app_name}_generate_info_plist" ]
        sources = [ "$gen_path/Info.plist" ]
        outputs = [ "{{bundle_contents_dir}}/Info.plist" ]
      }

      executable("${app_name}_generate_executable") {
        forward_variables_from(invoker, "*", [
                                               "output_name",
                                               "visibility",
                                             ])
        output_name =
            rebase_path("$gen_path/$app_name", root_build_dir)
      }

      code_signing =
          defined(invoker.code_signing) && invoker.code_signing

      if (is_ios && !code_signing) {
        bundle_data("${app_name}_bundle_executable") {
          deps = [ ":${app_name}_generate_executable" ]
          sources = [ "$gen_path/$app_name" ]
          outputs = [ "{{bundle_executable_dir}}/$app_name" ]
        }
      }

      create_bundle("${app_name}.app") {
        product_type = "com.apple.product-type.application"

        if (is_ios) {
          bundle_root_dir = "${root_build_dir}/$target_name"
          bundle_contents_dir = bundle_root_dir
          bundle_resources_dir = bundle_contents_dir
          bundle_executable_dir = bundle_contents_dir
          bundle_plugins_dir = "${bundle_contents_dir}/Plugins"

          extra_attributes = {
            ONLY_ACTIVE_ARCH = "YES"
            DEBUG_INFORMATION_FORMAT = "dwarf"
          }
        } else {
          bundle_root_dir = "${root_build_dir}/target_name"
          bundle_contents_dir  = "${bundle_root_dir}/Contents"
          bundle_resources_dir = "${bundle_contents_dir}/Resources"
          bundle_executable_dir = "${bundle_contents_dir}/MacOS"
          bundle_plugins_dir = "${bundle_contents_dir}/Plugins"
        }
        deps = [ ":${app_name}_bundle_info_plist" ]
        if (is_ios && code_signing) {
          deps += [ ":${app_name}_generate_executable" ]
          code_signing_script = "//build/config/ios/codesign.py"
          code_signing_sources = [
            invoker.entitlements_path,
            "$target_gen_dir/$app_name",
          ]
          code_signing_outputs = [
            "$bundle_root_dir/$app_name",
            "$bundle_root_dir/_CodeSignature/CodeResources",
            "$bundle_root_dir/embedded.mobileprovision",
            "$target_gen_dir/$app_name.xcent",
          ]
          code_signing_args = [
            "-i=" + ios_code_signing_identity,
            "-b=" + rebase_path(
                "$target_gen_dir/$app_name", root_build_dir),
            "-e=" + rebase_path(
                invoker.entitlements_path, root_build_dir),
            "-e=" + rebase_path(
                "$target_gen_dir/$app_name.xcent", root_build_dir),
            rebase_path(bundle_root_dir, root_build_dir),
          ]
        } else {
          deps += [ ":${app_name}_bundle_executable" ]
        }
      }
    }
  }

嗯,可以看到gn对生成app类型的软件包的支持并不直接,要生成一个这样的软件包,需要自己调用python脚本生成plist文件,手动创建app中的文件结构,再调用python脚本生成签名文件,要自己写一套这样的流程无疑是非常麻烦的,chrome在编译后可以自动生成app类型的软件包,我们不妨看一下能否利用chromium中的逻辑,在/chrome/BUILD.gn中:

# /chrome/BUILD.gn
...
  alert_helper_params = [
    "alerts",
    ".alerts",
    " (Alerts)",
  ]

  # Merge all helper apps needed by //content and //chrome.
  chrome_mac_helpers = content_mac_helpers + [ alert_helper_params ]

  # Create all helper apps required by //content.
  foreach(helper_params, content_mac_helpers) {
    chrome_helper_app("chrome_helper_app_${helper_params[0]}") {
      helper_name_suffix = helper_params[2]
      helper_bundle_id_suffix = helper_params[1]
    }
  }

  # Create app for the alert helper manually here as we want to modify the plist
  # to set the alert style and add the app icon to its resources.
  tweak_info_plist("chrome_helper_app_alerts_plist") {
    deps = [ ":chrome_helper_plist" ]
    info_plists = get_target_outputs(":chrome_helper_plist") +
                  [ "app/helper-alerts-Info.plist" ]
  }

  # Create and bundle an InfoPlist.strings for the alert helper app.
  # TODO(crbug.com/1182393): Disambiguate and localize alert helper app name.
  compile_plist("chrome_helper_app_alerts_plist_strings") {
    format = "binary1"
    plist_templates = [ "app/helper-alerts-InfoPlist.strings" ]
    substitutions = [ "CHROMIUM_FULL_NAME=$chrome_product_full_name" ]
    output_name = "$target_gen_dir/helper_alerts_infoplist_strings/base.lproj/InfoPlist.strings"
  }
  bundle_data("chrome_helper_app_alerts_resources") {
    sources = get_target_outputs(":chrome_helper_app_alerts_plist_strings")
    outputs = [ "{{bundle_resources_dir}}/base.lproj/{{source_file_part}}" ]
    public_deps = [ ":chrome_helper_app_alerts_plist_strings" ]
  }

  chrome_helper_app("chrome_helper_app_${alert_helper_params[0]}") {
    helper_name_suffix = alert_helper_params[2]
    helper_bundle_id_suffix = alert_helper_params[1]
    info_plist_target = ":chrome_helper_app_alerts_plist"
    deps = [
      ":chrome_app_icon",
      ":chrome_helper_app_alerts_resources",
    ]
  }
  ...
  foreach(helper_params, chrome_mac_helpers) {
    action("verify_libraries_chrome_helper_app_${_helper_target}") {
    ...
    }
  }
  
  bundle_data("chrome_framework_helpers") {
    foreach(helper_params, chrome_mac_helpers) {
      sources +=
          [ "$root_out_dir/${chrome_helper_name}${helper_params[2]}.app" ]
      public_deps += [ ":chrome_helper_app_${helper_params[0]}" ]
      ...
    }
  }
  ...

好吧,这里的处理更加复杂了,不过我们注意到,chrome将所有的helper app加入到了一个content_mac_helpers列表中,然后依次对这里面的内容进行了处理,再调用content_mac_helpers生成了app软件包,后面的内容主要是将helper app放到指定的位置,并使用链接的方式让它们和主程序共享了动态库。比较有意思的是,helper app的sources中并不包含程序入口(main()),而是在调用chrome_helper_app时加入了同一个函数入口chrome_exe_main_mac.cc。(试验了一下,每启动一种helper,就会进入一次main函数)

在这里的列表中加入一个自己要创建的helper app,我们发现在编译后的helper文件夹中确实出现了一个新的helper app(如上面所说,chromium为它们添加了统一的程序入口,即便我什么文件都不添加也能编译)。但我希望能创建一个相对独立的程序,不需要包含现在的这些库,也希望程序能有一个独立的入口,那么直接在这里的列表里添加是不太合适的,我们可以再研究一下这里面有哪些template是可以利用的。

看一下这里使用的几个主要的template

  • tweak_info_plist:让一个plist文件,或者将多个plist文件合并运行tweak_info_plist.py,生成一个新的plist文件,可以在args中设置要添加的属性。

  • compile_plist:作用和上面那个差不多,只不过输入的文件是以string的形式,它似乎可以替换文件中的一些变量,再写入plist文件中

  • mac_app_bundle:打包mac应用包,提供了多种类型的mac应用包的打包功能

重点在于mac_app_bundle,plist可以调用前面两个template进行配置,也可以完全自己写一份,调用方式类似于这样:

  mac_app_bundle("my_app") {
    output_name = "my_app"
    package_type = "app"

    info_plist_target = ":my_app_plist" 
    extra_substitutions = [
      "CHROMIUM_BUNDLE_ID=$chrome_mac_bundle_id",
      "CHROMIUM_HELPER_BUNDLE_ID_SUFFIX=my_app",
      "CHROMIUM_SHORT_NAME=$chrome_product_short_name",
      "CHROMIUM_HELPER_SUFFIX=my_app"
    ]
    
    no_default_deps = true
    deps = [
      ":my_app_src",
    ]
  }

然后将生成的app包拷贝到目标目录下

  action("copy_my_app_app") {
    script = "//app/build/copy_floder.py"

    args = [
      rebase_path("$root_out_dir/my_app.app"),
      rebase_path(
          "$root_out_dir/$name.app/Contents/MacOS/${chrome_product_full_name} Helper (my_app).app"),
    ]

    public_deps = [
      ":my_app",
    ]

    inputs = []
    outputs = [
      "$root_out_dir/$name.app/Contents/MacOS/${chrome_product_full_name} Helper (my_app).app",
    ]
  }

添加一个.mm文件,获取上面的app路径并调用launchApplication启动它:

int StartupMyHelperApp(pid_t parent_pid) {
  NSString *path = [[NSBundle mainBundle] bundlePath];
  NSArray *p = [path pathComponents];
  NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:p];
  [pathComponents addObject:@"Contents"];
  [pathComponents addObject:@"MacOS"];
  [pathComponents addObject:@"App Helper (my_app).app"];
  NSString *app_path = [NSString pathWithComponents:pathComponents];
  bool ret = [[NSWorkspace sharedWorkspace] launchApplication:app_path];
  return ret;
}

编译后启动主程序,然后关闭主进程,使用ps命令查看现有的进程:

可以看到现在我们创建的helper app仍在运行,成功通过这样的方法创建了一个独立进程