快速构建
如果是刚接触chromium源码开发,先从源码快速构建一个chrome浏览器。chromium是一个庞大的项目,构建chrome通常需要数小时时间,采用高配计算机可以缩短构建时间,建议计算机配置为内存不小于16G,硬盘空闲空间不小于100G。操作系统采用Ubuntu 22.04。如下是构建运行chrome的步骤:
- 获取depot_tools并把depot_tools加到PATH中
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
- 把depot_tools路径加到PATH中,其中/path/to/depot_tools是depot_tools的路径
export PATH=/path/to/depot_tools:$PATH
- 运行fetch命令获取代码及包括gn、ninja等工具在内的依赖,然后进入到chromium源码目录:~/chromium/src
mkdir ~/chromium && cd ~/chromium
fetch --nohooks chromium
cd ~/chromium/src
- 安装构建依赖
./build/install-build-deps.sh
- 运行hooks
运行install-build-deps安装构建以来后,必须运行chromium的hooks下载构建所需的其它依赖:
gclient runhooks
- 设置构建,创建一个构建目录:
gn gen out/Default
- 开始构建
autoninja -C out/Default chrome
- 运行chrome
out/Default/chrome
depot_tools
depot_tools包含了多个工具,这些工具对git进行了增强,利用这些工具可以更好的参与到chromium的开发工作中。用于管理chromium代码库,是一组工具。另外depot_tools还对gn和ninja做了封装,在任何平台下直接输入命令就能自动找到对应的二进制。depot_tools中关键工具有:
- gclient: meta-checkout工具,用来管理各自独立下载的模块间的依赖关系。运行这个工具的时候,会自动对depot_tools进行更新。
- fetch: fetch是对gclient的进一步封装,用于checkout一个项目。
- git cl: code review工具,与Rietveld或者Gerrit交互。
gclient工具
gclient用于涉及多个不同来源的组件的项目,对项目中组件的依赖关系进行管理,用于项目的状态查看和更新。gclient对git等工具进行了封装,支持git,gcs和cipd等三种依赖类型。gclient的配置文件在项目的top目录中,是名为.gclient的python脚本,定义了一个名为solutions的列表,格式如下:
solutions = [
{
"name": "src",
"url": "https://chromium.googlesource.com/chromium/src.git",
"managed": False,
"custom_deps": {},
"custom_vars": {},
},
]
- solutions: 由字典组成的数组,给出了要fetch的projects。
- hooks: 附加的hooks,sync的时候被执行。
- target_os: target组成的可选的数组,用于获取对应的target的依赖。
- cache_dir: 缓存目录,设置后可以从这个目录clone而不是从远端
一个solution是构成一个软件的所有组件的集合,这些组件按照特定的目录布局被检出和构建。solution由python字典定义,字段如下:
- name:checkout代码的目录名
- url:solution的地址,gclient从这个地址checkout出solution,solution中应包含一个DEPS文件,DEPS给出了需要检出的构成solution的所有代码组件及及其目录布局。
- deps_file: solution目录中用于依赖关系列表的文件名,默认为DEPS
- custom_deps: 字典类型,包括对DEPS中条目的定制化覆盖,比如采用本地代码而不是checkout
在检出的solution中有一个默认名字为DEPS的文件,也可以在.gclient文件中用deps_file定义为其它名称,DEPS文件内容是依赖关系列表,gclient也支持git submodules。DEPS文件指明了目录树中的源码包含了那些文件,是一个python脚本,定义了下面的变量:
- deps: 字典类型,用于说明子依赖,只有模块根目录中的DEPS有这个变量,定义了每个依赖子模块及下载这个子模块的url地址的映射。
- include_rules: 用于增加,移除已说明的规则,这些规则用于指明那些文件可以被include或者import。
- specific_include_rules: 作用于本目录及子目录的特定规则
- vars: 字典类型,用于定义变量。使得一次性覆盖一批修订版本更容易。
- hooks: sync之后执行。
.gclient和DEPS文件中有一个可选的列表类型的名字为hooks的变量,用于指出工作目录完成了sync,update等操作后需要执行的定制化的动作,可以通过--nohooks选项取消执行hooks,也可以通过gclient的runhooks命令主动执行hooks。hooks列表表项是字典类型
- "pattern": 正则表达式字符串,匹配的路径名对应的文件执行了checked out, updated, or reverted等动作,则执行hook。
- "action":要执行的hook命令及参数
- "name":可选,说明hook所属的group
一个hooks例子:
hooks = [
{ "pattern": "\\.(gif|jpe?g|pr0n|png)$",
"action": ["python", "image_indexer.py", "--all"]},
{ "pattern": ".",
"name": "gyp",
"action": ["python", "src/build/gyp_chromium"]},
]
depot_tools安装与更新
采用git获取depot_tools源码,确认已经安装了git,在要安装depot_tools的目录下执行:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
把depot_tools路径加到PATH环境变量,打开~/.bashrc文件,在文件末尾加入如下内容
export PATH=/path/to/depot_tools:$PATH
运行gclient时会首先自动更新depot_tools,自动更新可以关闭,有如下两种方法关闭自动更新:
- 设置环境变量DEPOT_TOOLS_UPDATE为0,打开~/.bashrc,加入如下内容
DEPOT_TOOLS_UPDATE=0
- 运行如下命令
./update_depot_tools.py --disable
关闭自动更新depot_tools后,可以手动更新depot_tools,命令如下:
update_depot_tools
源码获取与更新
chromium源码获取采用fetch命令,获取桌面版本chromium命令如下
fetch chromium
fetch命令完成后,当前目录下会有.gclient文件和src/目录,.gclient文件是gclient配置文件,src是源码目录。
fetch是对gclient的封装,用于代码的inits、checkouts、pulls、fetches等,fetch命令用法如下:
fetch [-h] [-n] [--nohooks] [--nohistory] [--force] [-p PROTOCOL_OVERRIDE] config ...
--nohooks选项指示fetch获取完成源码后不执行hooks,--nohistoryz选项指示fetch只做浅度克隆,不获取全量git历史,--force不查找现存的.gclient。-p给出采用的协议,默认为https。
config配置文件的名字,fetch命令根据这个名字在depop_tools/fetch_configs目录下获取对应的配置文件,配置文件是python代码,每个配置文件对应一个项目,比如桌面版chromium的配置文件chromium.py,android版chromium的配置文件android.py等,fetch命令会首先运行 <config>.py。
获取代码后,进到src目录下执行如下命令安装构建所需依赖并拉取源码依赖:
./build/install-build-deps.sh
gclient sync
要更新已经获取过的代码,先确认本地没有改动,并且git HEAD指向main:
git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
如果是detached状态:
git status
HEAD detached at origin/main
nothing to commit, working tree clean
通过checkout main命令让HEAD指向main:
git checkout main
Branch 'main' set up to track remote branch 'main' from 'origin'.
Switched to a new branch 'main'
更新本地代码:
git rebase-update
gclient sync
如果要构建指定版本,需要获取所有tags,然后checkout想要构建的tag
git fetch --tags
git checkout -b your_release_branch 74.0.3729.131 # or tags/74.0.3729.131
gclient sync --with_branch_heads --with_tags
元构建工具gn
gn(Generate-Ninja), 用于生成ninja构建文件的元构建工具。gn最初是chromium的一部分,目前已经从chromium剥离出来作为一个独立的项目。除了chromium,Fuchsia的元构建工具也采用gn。gn不支持把chromium作为子模块嵌入到其它项目中(参照https://github.com/vivaldi/Vivaldi-GN/blob/master/docs/Vivaldi-GN.md)。
在gn源码(https://gn.googlesource.com/gn)的docs目录下有更详细的文档,也可以通过如下命令获得gn的帮助文档:
gn help
gn相关命令,比如args命令打开编辑器修改参数,然后自动调用gn gen。修改参数的命令:
gn语法
gn使用简单的动态类型:
- 布尔型。
- 64位有符号整数。
- 字符串类型。
- 列表。
- 作用域,类似字典,只用于内建情况。
字符串
字符串类型采用双引号,采用反斜线作为逃逸字符,支持如下逃逸序列:
\" (引号)
\$ (dollars符号)
\\ (反斜线)
采用$实现简单的变量替代,变量名可以放到{}中,例如:
a = "mypath"
b = "$a/foo.cc" # b -> "mypath/foo.cc"
c = "foo${a}bar.cc" # c -> "foomypathbar.cc"
列表
列表支持追加,向列表追加列表不是把列表作为成员追加,而是把列表中的项逐一追加到列表:
a = [ "first" ]
a += [ "second" ] # [ "first", "second" ]
a += [ "third", "fourth" ] # [ "first", "second", "third", "fourth" ]
b = a + [ "fifth" ] # [ "first", "second", "third", "fourth", "fifth" ]
从列表中移除,“-”操作符查找并移除所有匹配项,从一个列表移除另一个列表是在第一个列表中查找并移除第二个列表的每一项,如果没有匹配项会抛出错误:
a = [ "first", "second", "third", "first" ]
b = a - [ "first" ] # [ "second", "third" ]
a -= [ "second" ] # [ "first", "third", "fourth" ]
列表支持0起始的下标,[]操作符用于只读,不能用于改变列表项的值:
a = [ "first", "second", "third" ]
b = a[1] # -> "second"
不能对一个列表变量进行赋值,但是可以清空后再赋值
a = [ "one" ]
a = [ "two" ] # Error: overwriting nonempty list with a nonempty list.
a = [] # OK
a = [ "two" ] # OK
条件语句
条件语句与C语言类似:
if (is_linux || (is_win && target_cpu == "x86")) {
sources -= [ "something.cc" ]
} else if (...) {
...
} else {
...
}
循环
遍历一个列表采用foreach,不过不建议采用遍历方式而是采用其它替代方式:
foreach(i, mylist) {
print(i) # Note: i is a copy of each element, not a reference to it.
}
函数
简单函数调用如下所示:
print("hello, world")
assert(is_win, "This should only be executed on Windows")
target调用后面跟着{}
代码块,运行时把代码块当做字典类型的参数来处理:
static_library("mylibrary") {
sources = [ "a.cc" ]
}
构建参数用于控制构建过程,构建参数以命令行参数等外部方式传给构建系统,构建参数在.gn中定义,定义构建参数的函数为declare_args,如下定义了enable_foo参数,默认值为true:
declare_args() {
enable_foo = true
}
default_args覆盖参数定义时的默认值:
作用域
文件或者函数调用后的`{}'代码块引入新的作用域,读取一个变量时,将从当前作用域开始沿着向上一层作用域的方向搜索。
Targets
target是构建graph中一个node,通常代表一个被生成的可执行文件或者库,target通过依赖关系关联在一起,内置的target类型如下:
- action:运行一个脚本生成文件
- action_foreach:针对sources中的每个文件运行一次给定脚本,具体参考 gn help action_foreach
- executable:生成一个可执行文件
- group:虚拟node,为一组targets命名
- shared_library:so文件
action一次性运行一个脚本并生成一个或者多个文件。action对于inputs和sources参数的处理是一样的,把inputs和sources传递给脚本需要放到args中,建议把脚本输入放到sources参数中,把其它python文件等运行脚本需要的内容放到inputs参数中。举例如下:
action("run_this_guy_once") {
script = "doprocessing.py"
sources = [ "my_configuration.txt" ]
outputs = [ "$target_gen_dir/insightful_output.txt" ]
# Our script imports this Python file so we want to rebuild if it changes.
inputs = [ "helper_library.py" ]
# Note that we have to manually pass the sources to our script if the
# script needs them as inputs.
args = [ "--out", rebase_path(target_gen_dir, root_build_dir) ] +
rebase_path(sources, root_build_dir)
}
和action相比action_foreach区别在于,针对sources中的每个文件执行一次脚本。
比如group的例子:
group("all") {
deps = [
"//project:runner",
"//project:unit_tests",
]
}
Configs
用于定义一个配置对象,配置对象用于target的编译的flags,inludes等等,用法如下:
config("myconfig") {
include_dirs = [ "include/common" ]
defines = [ "ENABLE_DOOM_MELON" ]
}
executable("mything") {
configs = [ ":myconfig" ]
}
命名及Label
有三种形式的文件和目录名:
相对:
"foo.cc"
"src/foo.cc"
"../src/foo.cc"
绝对:
"//net/foo.cc"
"//base/test/foo.cc"
系统绝对:
"/usr/local/include/"
"/C:/Program Files/Windows Kits/Include"
所有用于构成依赖关系的(targets, configs, toolchains)均可通过label标识,label是一个字符串,字符串的格式为:路径:label名字,
以source root为根的绝对路径的label:
"//base/test:test_support"
lable未给出toolchain的label则集成上一级的toolchain,如果需要交叉编译,则在label中包括所使用的toolchain的label,label后面紧跟一个括号括起来的toolchain的label:
"//base/test:test_support(//build/toolchain/win:msvc)"
相对路径的label:
"../net:url_request"
在同一个buildfile中,lable的路径部分可以省略:
":base"
构建过程
执行如下命令生成ninja文件:
gn gen out/Default
这个命令执行的流程:
- 在当前目录开始遍历目录树,查找.gn文件直到找到为止,将.gn所在目录设置为“source root”,解析.gn得到构建配置文件的名称,比如//build/config/BUILDCONFIG.gn。
- 运行构建配置文件,设置默认工具链?
- 加载source root下的BUILD.gn文件。
- 递归加载BUILD.gn文件,解析依赖关系
- 解析完一个目标依赖关系后,写对应的.ninja文件到out目录中
- 所有目标解析完成,输出build.ninja文件
上述过程涉及到如下文件及目录:
- gn配置文件:/.gn
- 构建配置文件:/build/BUILDCONFIG.gn
- 工具链相关配置在/build/toolchain目录中
- 通用的模版在/build/config目录中
在chromium的源码目录下加一个项目,在src下创建目录cwindowos,写好BUILD.gn,然后在src/BUILD.gn的gn_all中把[ "cwindowos" ]加到deps中
Template及grit和mojom
template用于定义模版规则,是gn进行代码重用的基本方法,template通常被扩展为一个或多个其它target类型,和使用其它类型的target一样,直接调用template的名字来使用template。template的定义一般在gni文件中,在gn文件中用import调用gni文件中的template。
调用template时会创建一个闭包,这个闭包包含调用template时作用域内所有变量和template代码块,当执行template的时候,闭包被执行,调用代码执行结果以“invoker”变量的方式被传递到template中,template通过invoker读取外部状态。template中当前目录是调用template的代码所在的目录。
GRIT(Google Resource and Internationalization Tool)是一个管理资源和简化本地化工作的工具
grit
tools/grit/repack.gni
mojom
mojo/public/tools/bindings/mojom.gni
构建工具ninja
ninja是一个专注于速度的小的构建工具。默认情况下,ninja在当前目录查找build.ninja并构建所有过期的目标,可以通过参数制定要构建的目标。
chromium并没有采用传统的构建工具,比如GNU Make,也没有采用传统的元构建工具,比如AutoMake,CMake,而是重新造了轮子。chromium的元构建工具是gn,构建工具是ninja。
第一步,调用元构建系统,生成ninja文件,第二步,运行ninja进行构建二进制目标
以上是传统的构建系统和元构建系统,chromium没有直接采用这些传统的系统,而是重新开发了元构建系统和构建系统,目前chromium采用的元构建系统是GN,采用的构建系统是ninja。对应的过程是:
gn gen path
gn args
ninja
第一步生成构建文件,第二部修改或者列出构建参数,第三部构建
gn gen path命令用于设置构建目录,与其它构建系统不同,采用GN需要设置构建目录,可以设置多个不同设置的构建目录。设置构建目录的同时会自动生成Ninja文件,每次构建会自动重新生成过期的Ninja文件,不需要再次手动整形gn gen命令。
如何构建OS,可以参照cef等,写自己的gn,获取chromium源码当作第三方组件进行构建
集成开发环境vscode
chromium开发需要一个集成开发环境,推荐使用vscode,配合clangd工具。
vscode设置
chromium源码tools/vscode目录下有vscode下进行chromium开发的配置,直接copy这些配置文件到src/.vscode目录下:
cd /path/to/chromium/src
mkdir .vscode
cp tools/vscode/*.json .vscode/
cp tools/vscode/cpp.code-snippets .vscode/
Chromium有推荐的vscode扩展,具体参照Chromium源码中的文档docs/vscode.md。
VSCode通过inotify检测文件变化,对于chromium这种庞大的代码,需要把max_user_watches设为最大值524288,先看看max_user_watches当前的值
cat /proc/sys/fs/inotify/max_user_watches
如果显示不是524288,修改/etc/sysctl.conf,加入如下内容
fs.inotify.max_user_watches=524288
运行sysctl使更改生效
sudo sysctl -p
Ctrl+,打开设置并搜索files.watcherExclude,设置files.watcherExclude来排除不必要的文件变化检测,默认情况下VSCode的files.watcherExclude设置如下:
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/*/**": true
}
clangd
clangd是针对C/C++的语言服务器,通过插件与VSCode等各种编辑器集成,实现自动补齐,导航,诊断,交叉引用,高亮等功能。
- 安装clangd
- 安装vscode的clangd插件
- 确认已经生成了ninja构建文件,然后手动生成编译数据
执行generate_compdb.py生成编译数据compile_commands.json,generate_compdb.py已在chromium源码中:
tools/clang/scripts/generate_compdb.py -p out/Default > compile_commands.json
可以通过配置文件对clangd进行配置,针对项目的配置文件为.clangd,在项目根目录下。比如通过配置跳过资源消耗极大的自动索引:
Index:
Background: Skip # Disable slow background indexing of these files.
参考资料
- depot_tools(7) Manual Page, http://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/depot_tools.html
- Managing Chromium dependencies, docs/dependencies.md
- GCS objects for chromium dependencies, docs/gcs_dependencies.md
- CIPD and 3pp for chromium dependencies, docs/cipd_and_3pp.md
- DEPS Files, buildtools/checkdeps/README.md
- gclient源码注释, depot_tools/gclient.py
- Visual Studio Code Dev, docs/vscode.md