认识 V8 引擎(3)- 编译 V8 9.1

上篇中,我们编译过了早期的 V8 1.2 版本。那个版本代码相对简单,是非常好的学习材料。这篇我们来编译当前最新稳定版 V8,即 V8 9.1.269.36。

本篇以 Mac 上编译为例,其他平台仅供参考

本篇主要分为几个部分:

  1. 下载并配置 depot_tools
  2. 下载 V8 源码
  3. V8 的编译参数
  4. 编译 x64 静态库
  5. 编译 arm64 静态库
  6. 总结

1. 下载并配置 depot_tools

目前 Google 很多源码都是通过 depot_tools 来管理的,比如 Chromium、Android、V8 源码。depot_tools 主要包含拉取代码的 fetch 命令,以及一系列的编译工具,比如:Ninja、gn等等。下载配置 depot_tools 看似简单,往往是出问题最多的一个步骤。出错原因往往是和网络和代理设置有关系,偶尔和 Python 的版本有关。

首先,找个目录把 depot_tools 下载下来:

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

然后将这个目录添加到环境变量中:

$ export PATH=/path/to/depot_tools:$PATH

接着,尝试直接执行 gclient 命令,是否报错,如果你看到类似如下输出,则恭喜你,第一步已经完成。

$ gclient
Usage: gclient.py <command> [options]

Meta checkout dependency manager for Git.

Commands are:
  config   creates a .gclient file in the current directory
  diff     displays local diff for every dependencies
  fetch    fetches upstream commits for all modules
  flatten  flattens the solutions into a single DEPS file
  getdep   gets revision information and variable values from a DEPS file
  grep     greps through git repos managed by gclient
  help     prints list of commands or help for a specific command
  metrics  reports, and optionally modifies, the status of metric collection
  pack     generates a patch which can be applied at the root of the tree
  recurse  operates [command args ...] on all the dependencies
  revert   reverts all modifications in every dependencies
  revinfo  outputs revision info mapping for the client and its dependencies
  root     outputs the solution root (or current dir if there isn't one)
  runhooks runs hooks for files that have been modified in the local working copy
  setdep   modifies dependency revisions and variable values in a DEPS file
  status   shows modification status for every dependencies
  sync     checkout/update all modules
  validate validates the .gclient and DEPS syntax
  verify   verifies the DEPS file deps are only from allowed_hosts

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -j JOBS, --jobs=JOBS  Specify how many SCM commands can run in parallel;
                        defaults to 8 on this machine
  -v, --verbose         Produces additional output for diagnostics. Can be
                        used up to three times for more logging info.
  --gclientfile=CONFIG_FILENAME
                        Specify an alternate .gclient file
  --spec=SPEC           create a gclient file containing the provided string.
                        Due to Cygwin/Python brokenness, it can't contain any
                        newlines.
  --no-nag-max          Ignored for backwards compatibility.

执行 gclient,默认会去更新 depot_tools 本身。如果你遇到更新出错,或者并不希望每次都更新。可以设置如下环境变量:

$ export DEPOT_TOOLS_UPDATE=0

如果 gclient 执行还是遇到各种奇怪的下载错误,一定是你的代理没有设置对。一般来说,命令里设置本地代理即可:

$ export http_proxy=http://127.0.0.1:1087;export https_proxy=http://127.0.0.1:1087;

我也曾经在 Windows 上配置过 depot_tools,比 Mac 上要复杂,中间还遇到几个坑。其中最大的一个坑就是:

务必在默认的命令行程序里运行! 务必在默认的命令行程序里运行! 务必在默认的命令行程序里运行!

千万不能在 PowerShell 或者什么 WSL2 里执行 gclient 等命令,不然错误信息会让你怀疑人生。

2. 下载 V8 源码

第一步配置成功真是太好了,夸张的说已经成功了一半。接下来下载 V8 源码就会简单很多了。首先找一个放 V8 源码的目录,最好创建一个新的目录。

$ mkdir v8; cd v8;
$ fetch v8

就是这么一个简单的命令:fetch v8,就可以把 v8 给下载下来,接下来如果你的网络没什么问题,剩下的就是耐心等待,你会看到一个类似如下的命令行输出:

$ fetch v8
Running: gclient root
Running: gclient config --spec 'solutions = [
  {
    "name": "v8",
    "url": "https://chromium.googlesource.com/v8/v8.git",
    "deps_file": "DEPS",
    "managed": False,
    "custom_deps": {},
  },
]
'
Running: gclient sync --with_branch_heads
...
[0:03:50] Still working on:
[0:03:50]   v8
1>Syncing projects:   0% ( 0/ 2)

我们看到,fetch 内部,执行了 gclient config 命令。它会在我们执行 fetch 的当前目录创建一个 .gclient 的文件:

solutions = [
  {
    "name": "v8",
    "url": "https://chromium.googlesource.com/v8/v8.git",
    "deps_file": "DEPS",
    "managed": False,
    "custom_deps": {},
  },
]

默认情况下,我们是不需要对 .gclient 做任何修改的。除非你想在你的 Mac或 Windows 上交叉编译 arm 平台 V8,本文第 5 点将详细介绍。

fetch v8 完成后,拉取的是最新的 V8 源码,在目录 v8/v8 里。如果需要更新指定版本的 V8,比如标题所示的 9.1 版本。则进入 V8 源码目录后,执行:

$ git checkout -b 9.1 -t branch-heads/9.1

这个命令会切换到 V8 9.1 当前最新的分支,比如当前最新的是:9.1.269.36。注意,仅仅切换了 V8 分支是不够的,还需要更新当前分支所有依赖的第三方库及工具。还需要执行如下命令更新所有的依赖。

$ gclient sync

3. V8 的编译参数

编译环境和源码都准备好了,可以准备编译了。首先,通过 gn 生成 Ninja 的编译配置:

$ gn gen out/x64.debug

这里要解释下 gn 和 Ninja 的关系了。首先是 Ninja,它是一个专注于速度的小型构建系统。它的诞生是源自 Google Chrome 团队的成员觉得使用 make 编译 Chrome 太慢了,于是开发了比 make 更快的 Ninja。使用 Ninja 后,Chrome 增量构建的时间降到了 6秒。可见,Ninja 相比 make,做了各种优化,比如依赖分析,这样增量构建时能编译尽量少的编译单元。

Ninja 的编译配置可读性很好,但是人肉写起来非常麻烦。于是很多人受不了,又写出一个又一个工具,用来生成 Ninja 的编译配置文件。于是 gn 这个工具就诞生了,并且成为了主力军。当然,类似的工具还有 CMake、meson。

执行了上面的命令后,会生成一个空的编译参数文件(out/x64.debug/args.gn),我们在这个文件里填具体的编译选项。

is_debug = true
target_cpu = "x64"

上面的选项只是举个例子,实际的编译选项非常之多。如果想查看所有的编译选项,可以执行如下命令:

$ gn args --list out/x64.debug

这里我列几个常用的选项介绍一下:

参数 说明
is_debug 默认 true,是否编 debug 版
target_cpu 必填,编译目标的平台: x64、arm、arm64
target_os 默认可不填,安卓平台可填:android
is_component_build 默认 true,是否编动态库
v8_static_library 默认 false,是否编静态库(编出多个.a)
v8_monolithic 默认 false, 编译单个静态库
v8_use_external_startup_data 默认 true,使用外部传入的 snapshot 文件
v8_enable_i18n_support 默认 true,国际化API支持,开启的话需要加载额外的 icu 库和文件。一般不开启
symbol_level 默认-1(自动),决定编译目标带多少符号信息,可减少编译目标大小。2: 常规符号。1: 最小符号,仅堆栈回溯。0: 不带符号,crash 时仅有最后一行堆栈
strip_debug_info 默认 fales,Android only,是否要 strip 掉 debug info,可减少编译目标大小
v8_enable_fast_mksnapshot 默认 false,mksnapshot 运行加速
use_custom_libcxx 默认 true,是否使用 V8 buildtools/third_party 里自带的那份自定义的 libc++ 库

4. 编译 x64 静态库

现在到了实战阶段了。如果需要编译出单个 V8 的静态库,并且希望编译的 .a 文件尽可能的小,可以使用如下的编译配置:

v8_monolithic = true
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_use_external_startup_data = false
v8_enable_i18n_support = false
symbol_level = 0
v8_enable_fast_mksnapshot = true
use_custom_libcxx = false

主要是设置编译 release 版本,并且把 symbol_level 设置成 0。

然后,执行编译指令:

$ autoninja -C out/x64.release v8_monolith

编译的 .a 文件会出现在:

$ ls -n out/x64.release/obj/libv8_monolith.a
-rw-r--r--  1 501  20  33044288  6 14 13:55 out/x64.release/obj/libv8_monolith.a

33M,已经够小了。

5. 编译 arm64 静态库

我们大多数情况下使用 V8 其实是在移动平台上,也就是 Android。当然,V8 现在也可以编译 iOS 版本,但由于不能有 JIT 功能,只能跑 JIT-less 版本,和 JavaScriptCore 比起来优势并不明显。所以这里只讨论下编译安卓的 V8。

编译安卓 V8 需要使用 ndk 和 android sdk 等,并且最好使用 v8 仓库里里的版本。还记得前面的 .gclient 文件吗?我们在文件最后面加上一行,指定 target_os:

solutions = [
  {
    "name": "v8",
    "url": "https://chromium.googlesource.com/v8/v8.git",
    "deps_file": "DEPS",
    "managed": False,
    "custom_deps": {},
  },
]
target_os = ['android']

然后再执行命令,更新所有依赖库和工具(会下载到 third_party/android_tools 目录):

$ gclient sync

准备编译配置:

$ gn gen out/arm64.release

编辑 out/arm64.release/args.gn(根据实际情况调整):

v8_static_library = true
is_debug = false
target_os = "android"
target_cpu = "arm64"
v8_enable_backtrace = true
v8_target_cpu = "arm64"
is_component_build = false
v8_monolithic = true
v8_use_external_startup_data = false
symbol_level = 0
strip_debug_info = true
v8_enable_i18n_support = false
v8_enable_fast_mksnapshot = true
use_custom_libcxx = false

这里设置了 use_custom_libcxx = false,是考虑到当前安卓工程使用的是 ndk 自带的 libcxx 库,比如: libc++_shared。如果 use_custom_libcxx = true,apk 里将会带两份 libcxx,将会出现不可预知的问题。

编译:

$ autoninja -C out/arm64.release v8_monolith

6. 总结

编译出 V8 单个静态库 libv8_monilith.a 后,配合 include 目录的 V8 头文件,就可以直接使用 V8 了。后面一篇将开始介绍 V8 的基础概念和基本使用。

微信扫一扫交流

作者:CoderZh
微信关注:hacker-thinking (代码随想)
本文出处:https://blog.coderzh.com/2021/06/14/v8-3/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。