跨平台:GN实践详解(ninja, 编译, windows/mac/android实战)
目录
一、概览
二、跨平台代码编辑器
三、GN入门
四、示范工程
五、关键细节
六、结语 [编译器选项]
其中前两部分是前缀部分,原本没有跨平台构建经验和知识的同学可以借助来帮助理解,后四部分则是讲述GN工程的基本结构、如何搭建一个GN构建的工程、以及关键的一些GN知识
一、概览
如何开始这个话题是我比较在意的,因为对于部分人而言,真正从思维和理解上切入这篇文章真正要阐述的点是有困难的。这在于跨平台编译和开发这块,如果没有一定的经历,可能会很难理解这些跨平台工具出现的真正意义,它们所要解决的问题是什么,所以这里我需要做一点前缀工作,如果对GN/GYP/CMake这类话题已经有上下文的同学可以直接跳过这部分赘述内容,同时也希望借助对这篇文章的理解,大家往后对于跨平台工具的理解不局限于GN,而是有自己的认知。在不断发展和变化的大潮中,工具始终是在演变,能够始终抓住紧要的精髓,见到陌生的工具时对它从根上有淡定从容的认知,以最小的精力消耗掌握它,最快发挥它的价值服务于我们,才是我们的取胜之道。
集成编译开发环境
以实际工作为例,我们在开发过程中需要接触到的有源代码工程管理、代码编辑、代码编译,而统一囊括了这几部分的开发环境就是集成开发环境,由于集成开发环境的存在,大部分人其实只需要重点关注代码编辑这块,工程管理和编译集成开发环境已经内置做了绝大部分工作,所以理所当然也就容易忽略它们,我们也有必要重新认识下这几部分工作。
工程管理:源代码包含、模块输出设置(动态库/静态库/可执行文件、模块依赖关系配置、编译选项设置、链接选项设置、生成后执行事件……等非常多的设置。
代码编辑:自动补齐、格式对齐、定义跳转、关键字高亮、代码提示……等我们编写代码时用着很人性化的功能。
代码编译:语法语音分析、代码优化、错误提示、警告提示、最终生成二进制代码和符号表。
各操作系统应用开发的集成开发环境
Windows应用开发:Visual Studio,简称VS,是微软提供和支持的集成开发编译环境,用于开发Windows上应用。
Mac/iOS应用开发: Xcode,是苹果提供和支持的集成开发编译环境,用于开发Mac和iOS应用。
Android应用开发:Android Studio,简称AS,是谷歌提供和支持的集成开发环境,用于开发Android应用。
Ubuntu等Linux应用开发:没有集成开发环境,工程管理使用CMake/GN/GYP…各类工具(也可以使用人脑)、代码编辑使用vim等工具,编译器使用GCC。
但实际上,这些集成开发环境都不约而同的将编译相关的细节很好的隐藏起来了,因为这块实在是太晦涩和繁琐,这里我们需要有一个很清晰的认知,那就是这些集成开发环境之下,隐藏的编译器基本上只有:微软的msvc,GNU的gcc,苹果的clang+llvm这三大类,让我们再次展开这几个集成开发环境,一窥的它的全貌
其中蓝色部分就是跨平台工具的主战场,旨在脱离各平台开发环境的限制,统一进行工程管理和工程设置,最终由工具自动生成统一的各平台makefile文件,调用各平台编译器完成编译。
沿途的风景
在通往终点的路上,总有一些风景值得欣赏,如果有感兴趣的同学,可以品味下编译器的技术历史,以及gcc+llvm这种畸形产物的闲闻逸事:
1.msvc,对于微软而言,闭源是它一直以来的做派,如今有所改善,而编译器则是那时候的产物,所以理所当然,宇宙最强IDE中的编译器,只限于windows这个平台上
2.gcc,是GNU开发的编程语言编译器,以GPL及LGPL许可证所发行的自由软件,开放给所有人,是最通用和广泛的编译器
3.gcc+llvm,苹果早期使用的gcc,gcc非常通用,支持多种语言,但对苹果官方提出的Objective-C编译器优化的诉求爱答不理,导致苹果只能想办法自己来做,一般而言,按照保守的重构做法一部分一部分渐进式替换原有模块是比较理性的思路,苹果也是采用这种策略,将编译器分为前后两端,前端做语义语法分析,后端做指令生成编译,而后端则是苹果实现自己愿望的第一步,由此llvm横空出世。
后端使用llvm,在编译期间针对性优化,例如以显卡特性为例,将指令转换为更为高效的GPU指令,提升运行性能。
前端使用gcc,在编译前期做语义语法分析等行为
4.clang+llvm,由于gcc语义语法分析上,对于Objective-C做得不够细致,gcc+llvm这种组合无法提供更为高级的语法提示、错误纠正等能力,所以苹果开始更近一步,开发实现了编译器前端clang,所以到目前为止xcode中缺省设置是clang+llvm,clang在语义和语法上更为先进和人性化,但缺点显而易见,Clang只支持C,C++和Objective-C,Swift这几种苹果体系内会用到的语言。
跨平台开发遇到的问题
一般而言,如果一个应用需要在各个操作系统上都有实现,基本的方式都是将大部分能复用的代码和逻辑封装到底层,减少冗余的开发和维护工作,像微信、QQ、支付宝等知名软件,都是这种方式。由于各个操作系统平台的集成开发环境不同,这部分底层工程需要在各个集成开发环境中搭建和维护工程、编译应用,这样就会产生大量重复工作,任何一个源码文件的增加/删除、模块调整都会涉及到多个集成开发环境的工程调整。这个时候,大多数人都会想,如果有这么一个工具,我们用它管理工程和编译,一份工程能够自动编译出各个平台的应用或底层库,那将是多么美好的一个事情。
幸运的是,已经有前人替我们感受过这个痛苦的过程,也有前人为我们种好了树,我们只需要找好一个合适的树,背靠着它,调整好坐姿,怀着无比感恩的心情在它的树荫下乘凉。树有好多课,该选哪一颗,大家兴趣至上自行选择,这里我们主要介绍我个人倾向的GN这一棵。
原始跨平台:
编写makefile文件,使用各平台上的make(微软的MS nmake、GNU的make)来编译makefile文件,这种做法的缺点是各平台的make实现不同,导致这种原始的做法其实复用度并不高,需要针对各平台单独编写差异巨大的makefile文件,那为什么要介绍它呢,因为这是跨平台的根,所有跨平台工具,最终都是要依赖各平台应用的集成开发环境的编译器来执行编译,这是固定不变的,也就是说各平台的编译,最终还是需要各平台的makefile,这一点是无法逃避的,而怎么由人工转为自动化,才是跨平台编译的进阶之路。
进阶跨平台:
使用cmake,编写统一的makefile文件,最后由cmake自动生成各平台相关的makefile文件执行编译,这一点上,cmake已经是比较好的跨平台工具了,一般的跨平台工程基本已经满足需求了。
现代跨平台:
当工程规模增大到难以想象的量级时,编译速度和工程模块的划分变得尤为重要,其中chromium工程就遇到这两个问题,于是最初诞生了gyp,最后演化升级为gn,其旨在追求工程更加清晰的模块和结构呈现,以及更快的编译速度。前者通过语法层面实现,后者则依靠ninja来提升编译速度,因为大型工程的编译,很大一部分时间都花在了源文件编译依赖树的分析这块,而ninja更像是一个编译器的预处理,其主要目的是舍弃gcc、msvc、clang等编译器在编译过程中递归查找依赖的方式,因为这里存在很多重复的依赖查找,而ninja改进了这一过程,提前生成编译依赖树,编译期间按照编译依赖树的顺序依次编译,这样就大大减少了编译期间杂乱的编译顺序造成的重复依赖关系查找。
关于一点说明:
本文主要针对底层库领域的跨平台构建,这也是比较常见和通用的跨平台应用方式,但不排除有例外,像Qt就是一套从底层开发到UI框架,再到代码编辑、工程管理的C++整体跨平台解决方案,它的涵盖面更大,只有应用层使用Qt的UI框架时才能发挥它的真正威力(有兴趣的同学可以研究它,目前我所在团队研发的CCtalk客户端Windows和Mac,以及即将外发的Chromebook版都是使用它,我也打算在一个合适的时机从我最擅长的UI框架领域来介绍Qt的实战应用),而这篇文章寻求的是目前为止更通用化的跨平台方式,也就是C++跨平台底层+原生UI应用层的组合方式,所以本文跨平台的切入重点也是针对底层库这一块,做得好的应用,一般会把网络、数据、业务上下文状态管理归属到底层由跨平台实现,这部分可以达到整个应用程序代码量的60~70%,完成这块的复用是非常值得的。
二、跨平台代码编辑器
gn解决了跨平台编译问题,但是各平台的代码编辑,并不属于底层C++跨平台构建工具的范畴(全框架C++流程的Qt例外)。一种做法是,通过gn生成各个平台的工程(xcode工程、vs工程)然后再进行代码编写,源文件和工程模块的修改需要另外同步到gn工程文件中;另一种做法是使用vscode。显而易见,使用vscode+gn是最佳选择,这样就相当于一个各平台统一的集成开发环境,这里我给大家预览下vscode和gn配合之下的预览图:
代码编写使用vscode,有各种插件提供了语法提示和检测、自动补齐、高亮等编辑器功能
代码编译使用gn,在vscode中直接有内置的命令行,直接运行编译脚本执行编译,编译脚本中是gn的编译命令的调用
在vscode+gn的使用过程中,我们会慢慢体会到代码编辑、编译的白盒流程,而不是以前的黑盒,对编辑和编译流程的理解也将越来越深刻,这是软件开发越来越上层的当下,最难能可贵的地方,是抱有一颗求知之心的人最珍视的东西。*
三、GN入门
官方文档
https://gn.googlesource.com/gn/+/master/docs
GN虽然有非常完善和详细的官方文档,但如果要真正搭建一个跨平台的工程,则往往寸步难行,这在于GN终究只是chromium的附产物,在于更好的服务chromium工程,所以缺少一些必要详细的入门模版,尤其是在各个编译器下的编译、链接选项设置这块,需要完全理解各个编译器的编译参数和设置,这对于我们的精力来说,几乎是不可能一下子就能了解清楚的,而这些编译选项往往是一次性工作,一旦配置好了,以后都不需要修改,或者仅需要修改个别选项。好在有大量成熟的跨平台开源项目,以chromium为例,我们从中扒出GN的最小集,再应用到我们一个自定义的工程中,搞清楚各个环节的含义,那么基本就算已经掌握了GN了。
示例工程
为了能够对GN有一个全貌的了解,我准备了一个简单的demo工程,请大家务必下载下来,后续的介绍都是基于这个示例工程来做
腾讯微云:https://share.weiyun.com/5ymLr5u
如果大家遇到下载不了或者最终编译错误的异常情况可以评论中联系我,目前已在windows、mac、android编译环境验证
鸟瞰一个GN工程
我们先从工程的全貌来看,这是我们demo工程的全局视图,GN组织一个跨平台工程分为3大块:
第1部分:整体工程入口,这部分常年都不用做修改
第2部分:GN通用文件,这部分常年都不用做修改
第3部分:GN源代码工程文件,这部分与平常我们在集成开发环境中类似,源文件的组织和管理就在这个部分
详解各GN文件职责
四、示范工程
一般而言,这一部分仔细阅读后,基本就可以依葫芦画瓢使用起GN了,更加细节的部分则需要靠大家阅读官方文档以及后续的第五部分内容
准备环境
1.安装vscode(主要是方便大家阅读工程结构,也可以跳过这一步,只是会增加工程的理解难度而已)
vscode的使用非常简单,指定打开某个文件夹,然后整个文件夹的目录结构就自动呈现在vscode的左侧了,同时插件安装异常方便,直接在vscode左侧最后一个tab中搜索关键字就会有插件的在线安装提示,根据提示安装好安装C++插件、gn插件等插件后,就可以作为代码编辑器编写代码了。
2.安装python3
关于python的语法,简单了解就好,遇到看不懂的稍微查下语法,脚本语言总是易于理解和使用的,大家要有信心。
工程目录结构
各平台编译脚本执行
windows:在gn_project目录下,命令行中运行(vscode内置的更方便)命令build_for_win.bat debug
mac:gn_project目录下,命令行中运行(vscode内置的更方便)命令./build_for_mac.sh debug
android: gn_project目录下,命令行中运行(vscode内置的更方便)命令./build_for_android.sh debug arm
mac平台编译入口脚本
# ninja files
$gn gen $build_cache_path --args="is_debug=$debug_mode target_cpu=“x64"”
if [ $? != 0 ]; then
echo “generate ninja failed”
exit 1
fi
echo -
echo -
echo ---------------------------------------------------------------
echo 第4步:开始ninja编译…
echo ---------------------------------------------------------------
# build
$ninja -C $build_cache_path > $log_path
if [ $? != 0 ]; then
echo “build failed. log: $log_path”
exit 1
fi
windows平台编译入口脚本
./build_for_win.sh 和 ./build_for_win.py,关键脚本如下(只是截取部分关键代码,旨在让大家看到各平台下gn编译命令的调用):
# --------------------------------------------------------------------------------
# 3. build kernel
# --------------------------------------------------------------------------------
# 1) generate ninja file for build
# 2) build
# 3) print end log
# --------------------------------------------------------------------------------
print(“build kernel …”)
gn_cmd = ‘"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp)
proc = subprocess.run(gn_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False)
stdout = buildtools.console_to_str(proc.stdout)
stderr = buildtools.console_to_str(proc.stderr)
if ‘Done.’ not in stdout:
print(stdout)
print(stderr)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
sys.exit(1)
ninja_cmd = ‘"{}" -C “{}”’.format(ninja, build_cache_path)
proc = subprocess.run(ninja_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False)
stdout = buildtools.console_to_str(proc.stdout)
stderr = buildtools.console_to_str(proc.stderr)
if proc.returncode != 0:
print(stdout)
print(stderr)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
sys.exit(1)
with open(os.path.join(build_cache_path, ‘build.log’), ‘w’) as log:
log.write(stdout)
android平台编译入口脚本
./build_for_android.sh,关键脚本如下(只是截取部分关键代码,旨在让大家看到各平台下gn编译命令的调用):
echo -
echo -
echo ---------------------------------------------------------------
echo 第5步:使用gn生成ninja文件
echo ---------------------------------------------------------------
# ninja file
$gn gen $build_cache_path --args="
target_os = “android”
target_cpu = “$target_config”
use_qt_for_android = false
ndk = “$ndk”
qt_sdk = “$qt_sdk”"
if [ $? != 0 ]; then
echo “generate ninja failed”
exit 1
fi
echo -
echo -
echo ---------------------------------------------------------------
echo 第6步:使用ninja开始编译…
echo ---------------------------------------------------------------
# build
$ninja -C $build_cache_path > $log_path
if [ $? != 0 ]; then
exit 1
fi
整个工程的各个代码模块
工程总共有多少模块,需要在./BUILD.gn文件中配置,而各个模块的工程则在各个模块中,关键代码如下(只是截取部分关键代码,旨在呈现整体工程各个模块):
具体代码模块
以整体工程文件./BUILD.gn中各个模块的配置为例,"//system_wrappers:system_wrappers"表示在system_wrappers目录下,寻找BUILD.gn文件,在这个BUILD.gn文件中定义了system_wrappers模块,以及该模块的源文件、编译选项设置等,具体代码如下:
library_template.gni文件
这是一个GN模版文件,是我们自定义的一个GN文件,文件名可以根据大家的喜好自行修改,确保在要使用的地方import进来即可,每个代码模块的编译选项设置其实是有蛮多编译选项和工程定义需要设置,而这些编译选项和定义的设置其实是重复的,如果每个模块都写一遍,会比较冗余,所以我们抽取了公共的模版,使得各个代码模块的BUILD.gn文件可以只关注源代码包含、附件包含目录、附加包含库等工程相关的事项。
五、关键细节
这是GN比较进阶的一部分,需要参考官方文档中语法部分进行解读
https://gn.googlesource.com/gn/+/master/docs/reference.md
官方语法文档的查阅方法是,“关键字 + :”,例如需要看template的定义时,文档中搜索“template:”即可出现详细的定义和解释
1.template
template(模版)的作用是抽取公共gn代码,节省整体的gn代码量,使得工程文件更容易阅读,以library_template.gni为例,有几个关键语法我们需要熟知:
模版中的变量和模版实例之间的数据传递,是通过invoker来承载,例如以sources的值为例,模版中可以通过sources = invoker.sources来传递值
模版实例的名称,则通过target_name来承载,例如,我们定义了模版libaray,那么library(“aysnevent”)的target_name值就是”aysnevent"
template(“library”) {
assert(defined(invoker.sources), “Need sources in $target_name listing the source files.”)
type = “static_library”
if (is_win) {
type = “shared_library”
}
if (is_mac || is_android) {
type = “source_set”
}
target(type, target_name) {
configs += [
“//:qt_config”,
]
sources = invoker.sources
# output_prefix_override = true
output_dir = “$root_out_dir”
lib_dirs = [
rebase_path("$output_dir/libs"),
]
if (type == “static_library”) {
output_dir += “/libs”
}
if (defined(invoker.deps)) {
if (defined(deps)) {
deps += invoker.deps
} else {
deps = invoker.deps
}
}
if (defined(invoker.data_deps)) {
if (defined(data_deps)) {
data_deps += invoker.data_deps
} else {
data_deps = invoker.data_deps
}
}
if (defined(invoker.include_dirs)) {
if (defined(include_dirs)) {
include_dirs += invoker.include_dirs
} else {
include_dirs = invoker.include_dirs
}
}
if (defined(invoker.configs)) {
if (defined(configs)) {
configs += invoker.configs
} else {
configs = invoker.configs
}
}
if (defined(invoker.public_configs)) {
if (defined(public_configs)) {
public_configs += invoker.public_configs
} else {
public_configs = invoker.public_configs
}
}
if (defined(invoker.cflags)) {
if (defined(cflags)) {
cflags += invoker.cflags
} else {
cflags = invoker.cflags
}
}
if (defined(invoker.cflags_cc)) {
if (defined(cflags_cc)) {
cflags_cc += invoker.cflags_cc
} else {
cflags_cc = invoker.cflags_cc
}
}
if (defined(invoker.del_configs) && defined(configs)) {
configs -= invoker.del_configs
}
if (defined(invoker.del_cflags_cc) && defined(cflags_cc)) {
cflags_cc -= invoker.del_cflags_cc
}
if (defined(invoker.libs)) {
if (defined(libs)) {
libs += invoker.libs
} else {
libs = invoker.libs
}
}
if (defined(invoker.defines)) {
if (defined(defines)) {
defines += invoker.defines
} else {
defines = invoker.defines
}
}
}
2.BUILD.gn和xxx.gni
BUILD.gn一般作为模块的工程入口文件,可以配合.gni文件来划分源码或模块。
当模块比较大时,可以用.gni来划分内部子模块,减轻工程文件的负担;
当多个模块之间差异很小时,可以利用BUILD.gn来统一管理这些模块的设置,利用.gni来专属负责各个模块源文件管理。
以./modules/BUILD.gn为例,它包含了多个代码模块,各个模块的源文件和工程配置则在各个.gni中管理
3.source_set/static_library/shared_library
source_set:编译后的代码集合,它与静态库的唯一区别是没有链接的符号文件,就是一般编译后生成的.o文件,目前我们demo工程里头,非windows系统上使用的是source_set(具体参见 library_template.gni),编译完成后,使用ar和ranlib命令重新打包成带符号表的.a静态库
特别说明:最终打包出来的是.a库,那么为什么demo中不直接用static_library呢,这是因为,整个工程的各个模块,由于没有一个统一的地方调用这些模块,这就会导致编译出来的结果是各个分散的.a库,使用source_set,然后再手动打包生成符号表最终可以生成一个统一的.a库
static_library:静态库,windows上是.lib库,其他平台是.a库
shared_library: 动态库,windows上是.dll库,mac和iOS上是.dylib库,andorid是.so库
4.”.gn“根文件
这是一个入口设置的文件,是GN中的固定规则文件,会自动被GN读取,但由于它没有文件名,只有扩展名,各个系统上如果没有打开文件夹选项中查看扩展名的设置,会看不到,这里需要注意。如果使用的vscode,则不需要担心这个问题,在vscode中可以默认看到该文件
5.windows上对XP支持
如果windows上安装的是VS2010以上版本,默认编译出来的结果是不支持XP系统的,如果需要支持,则需要手动增加支持选项,在./gn/toolchain/BUILD.gn中对应修改即可
具体参见关键代码(只截取了部分关键代码)
修改1:./gn/BUILDCONFIG.gn中declare_args中增加XP参数声明“win_xp = false”,这样windows上调用ninja编译命令时,传入是否支持XP的选项
# --------------------------------------------------------------------------------
# 3. build kernel
# --------------------------------------------------------------------------------
# 1) generate ninja file for build
# 2) build
# 3) print end log
# --------------------------------------------------------------------------------
print(“build kernel …”)
gn_cmd = ‘"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp)
修改2:./gn/toolchain/BUILD.gn中toolchain("msvc”) 下tool(“cc”)中增加”/D_USING_V110_SDK71_”编译选项
# Label names may have spaces so pdbname must be quoted.
command = “cmd /c “$env_setup $cc_wrapper $cl /FS /nologo /showIncludes /FC @$rspfile /c {{source}} /Fo{{output}} /Fd”$pdbname”""
if (win_xp) {
command += " /D_USING_V110_SDK71_"
}
修改3:./gn/toolchain/BUILD.gn中toolchain("msvc”) 下tool(“cxx”)中增加”/D_USING_V110_SDK71_”编译选项
# Label names may have spaces so pdbname must be quoted.
command = “cmd /c “$env_setup $cc_wrapper $cl /FS /nologo /showIncludes /FC @$rspfile /c {{source}} /Fo{{output}} /Fd”$pdbname”""
if (win_xp) {
command += " /D_USING_V110_SDK71_"
}
修改4:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“alink”)中增加”/SUBSYSTEM:WINDOWS,5.01”编译选项
command = “cmd /c “$env_setup lib/nologo/ignore:4221arflags/OUT:output@ lib /nologo /ignore:4221 {{arflags}} /OUT:{{output}} @lib/nologo/ignore:4221arflags/OUT:output@rspfile””
if (win_xp) {
command += " /SUBSYSTEM:WINDOWS,5.01"
}
修改5:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“solink”)中增加”/SUBSYSTEM:WINDOWS,5.01”编译选项
command = “cmd /c “$env_setup $lnk /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:$pdbname @$rspfile””
if (win_xp) {
command += " /SUBSYSTEM:WINDOWS,5.01"
}
修改6:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“link”)中增加”/SUBSYSTEM:WINDOWS,5.01”编译选项
command = “cmd /c “$env_setup $lnk /nologo /OUT:$exename /PDB:$pdbname @$rspfile””
if (win_xp) {
command += " /SUBSYSTEM:WINDOWS,5.01"
}
6.是否生成调试信息
release编译选项下,生成调试信息的情况下,才会对应生成符号文件,用于crash时分析崩溃,例如windows上的pdb、mac上的dSYM
为了调用方便,我们在./gn/BUILD.gn中自定义了配置选项,具体代码如下
# -------------------------------------------------------------------------
# debug_symbols
# 调试符号文件
# -------------------------------------------------------------------------
config(“debug_symbols”) {
# It’s annoying to wait for full debug symbols to push over
# to Android devices. -gline-tables-only is a lot slimmer.
if (is_android) {
cflags = [
“-gline-tables-only”,
“-funwind-tables”, # Helps make in-process backtraces fuller.
]
} else if (is_win) {
cflags = [ “/Zi” ]
ldflags = [ “/DEBUG” ]
} else {
cflags = [ “-g” ]
}
}
然后在./gn/BUILDCONFIG.gn中,将该自定义配置增加到gn的缺省配置中,具体代码如下
# Default configs
default_configs = [
“//gn:default”,
# “//gn:no_exceptions”,
# “//gn:no_rtti”,
# “//gn:warnings”,
“//gn:warnings_except_public_headers”,
]
if (!is_debug) {
default_configs += [ “//gn:release” ]
}
default_configs += [ “//gn:debug_symbols” ]
default_configs += [ “//gn:extra_flags” ]
7.dispatch_for_ide.py
这是我们自定义的编译完成后执行动作,通过python实现,主要实现非windows下.a库打包和生成文件同步到指定输出目录,这里有几个基础的概念需要理解,才能明白这个编译完成后事件调用的机制
action:gn中的执行动作,它用于定义一个执行的脚本,一般配合python来使用,在./BUILD.gn中,我们定义了action("dispatch_for_ide”),从而指定了需要执行的脚本是dispatch_for_ide.py。
action中的deps:指定脚本的执行需要在这些依赖项完成之后,而我们定义的action中依赖项是project,也就是整个工程编译完成后才能开始执行脚本。
我们可以从过官方文档中看到具体的定义描述(页面中查找关键字“action:”即可看到)
the “deps” and “public_deps” for an action will always be completed before any part of the action is run so it can depend on the output of previous steps.
为了便于理解,我们先看下action("dispatch_for_ide”)的调用代码,代码位于./BUILG.gn
group(“ccore”){
deps = projects
deps += [ “//:dispatch_for_ide” ]
}
然后,我们再看action("dispatch_for_ide”)具体定义代码,代码位于./BUILG.gn中
# 编译完成后执行动作
action(“dispatch_for_ide”) {
arg = [
“–kernel”, rebase_path("//"),
“–outpath”, rebase_path("//out"),
“–cachepath”, rebase_path("$root_out_dir"),
]
if (is_debug) {
arg += [ “–buildmode”, “Debug” ]
} else {
arg += [ “–buildmode”, “Release” ]
}
if (is_mac) {
arg += ["–platform", “mac”]
} else if (is_ios) {
arg += ["–platform", “ios”]
} else if (is_android) {
arg += ["–platform", “android”]
} else if (is_win) {
arg += ["–platform", “win”]
}
arg += ["–targetcpu", “${target_cpu}”]
script = “dispatch_for_ide.py”
outputs = [ “$root_out_dir/libccore.a” ]
args = arg
deps = projects
}
8.data_deps和deps
data_deps:
非链接依赖关系,如果依赖的模块是一个静态库,则需要代码中手动通过命令引入对应lib库,链接时才能成功,
例如,代码中#pragma comment(lib, “xxx.lib”),一般用于特指windows上隐式链接(静态链接)的dll时,需要代码中如是操作
Specifies dependencies of a target that are not actually linked into the current target. Such dependencies will be built and will be available at runtime.
This is normally used for things like plugins or helper programs that a target needs at runtime.
Note: On iOS and macOS, create_bundle targets will not be recursed into when gathering data_deps. See “gn help create_bundle” for details.
deps:
私有链接依赖关系,如果依赖的模块是一个静态库,则不需要代码中再人工链入指定的lib库文件,一般特指依赖的静态库
Specifies private dependencies of a target. Private dependencies are propagated up the dependency tree and linked to dependent targets, but do not grant the ability to include headers from the dependency. Public configs are not forwarded.
9.Android下编译环境
Android下编译,需要额外的环境依赖设置,具体在./build_for_android.sh中设置,然后在./gn/BUILDCONFIG.gn中应用
./build_for_android.sh中对应代码如下
ndk="/Library/android-ndk-r16b"
PATH=$PATH:$ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin
echo ndk路径:$ndk
#qt_sdk="/Users/taoshu/Qt5.9.6/5.9.6"
#echo qt路径:$qt_sdk
…
# ninja file
$gn gen $build_cache_path --args="
target_os = “android”
target_cpu = “$target_config”
use_qt_for_android = false
ndk = “$ndk”
qt_sdk = “$qt_sdk”"
if [ $? != 0 ]; then
echo “generate ninja failed”
exit 1
fi
NDK路径设置:
编译针对Android环境的目标文件时,NDK是必不可少的设置,它提供了各种针对Android标准库以及交叉编译的工具集
Qt路径设置:
应用层是Qt框架时:
需要将编译参数use_qt_for_android设置为true,并设置好Qt路径,这是由于Qt的UI线程和java主线程不在一个线程,是自定义实现,所以多线程转异步处理依赖Qt的消息循坏实现
应用层是Java原声框架时:
需要将编译参数use_qt_for_android设置为false即可,Qt路径可以忽略
10.Windows下Visual Stuido的版本
由于GN中需要指定windows下编译时使用的VS版本,这是GN之外我们需要做的事情,目前是通过python脚本去检查指定安装目录,从而自动获取本机的VS版本,这块理论上不会有问题,但电脑环境千差万别,难免会有问题,所以demo工程中,直接强制指定使用的是VS2015,如果大家需要修改VS版本,需要在如下脚本文件中做稍许调整
./build_for_win.py:其中有参数指定强制使用VS2015
# --------------------------------------------------------------------------------
# 3. build kernel
# --------------------------------------------------------------------------------
# 1) generate ninja file for build
# 2) build
# 3) print end log
# --------------------------------------------------------------------------------
print(“build kernel …”)
gn_cmd = ‘"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp)
proc = subprocess.run(gn_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False)
stdout = buildtools.console_to_str(proc.stdout)
stderr = buildtools.console_to_str(proc.stderr)
./gn/BUILDCONFIG.gn:其中有根据用户安装目录去搜寻VS版本和VS环境的代码
# *********************************************************************************
# Config msvc builder
# 配置MSVC编译,首先会尝试从系统中自动寻找vc编译器,
# 没有匹配到结果时,再从缺省硬编码配置路径中读取
# *********************************************************************************
msvc = win_vc_ver
if (target_os == “win”) {
# By default we look for 2017 (Pro & Community), then 2015. If MSVC is installed in a
# non-default location, you can set win_vc to inform us where it is.
vc_2017_pro_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/Professional/VC”
vc_2017_com_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC”
vc_2017_bt_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC”
vc_2015_default = “C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC”
vc_2013_default = “C:/Program Files (x86)/Microsoft Visual Studio 12.0/VC”
if (win_vc == “”) {
if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py",
[ “$vc_2017_pro_default” ],
“trim string”)) {
win_vc = vc_2017_pro_default
msvc = 2017
} else if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py",
[ “$vc_2017_com_default” ],
“trim string”)) {
win_vc = vc_2017_com_default
msvc = 2017
} else if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py",
[ “$vc_2017_bt_default” ],
“trim string”)) {
win_vc = vc_2017_bt_default
msvc = 2017
} else if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py",
[ “$vc_2015_default” ],
“trim string”)) {
win_vc = vc_2015_default
msvc = 2015
} else if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py",
[ “$vc_2013_default” ],
“trim string”)) {
win_vc = vc_2013_default
msvc = 2013
}
}
assert(win_vc != “”) # Could not find VC installation. Set win_vc to your VC directory.
if (msvc == “”) {
if (“True” == exec_script("//gn/BUIDCONFIG_win_check_vcdir.py", [ “$win_vc/Tools” ], “trim string”)) {
msvc = 2017
} else {
msvc = 2015
}
}
}
if (target_os == “win”) {
if (msvc == 2017 && win_toolchain_version == “”) {
win_toolchain_version = exec_script("//gn/BUIDCONFIG_win_highest_version_vcdir.py",
[
“$win_vc/Tools/MSVC”,
“[0-9]{2}.[0-9]{2}.[0-9]{5}”,
],
“trim string”)
}
if (win_sdk_version == “”) {
win_sdk_version = exec_script("//gn/BUIDCONFIG_win_highest_version_vcdir.py",
[
“$win_sdk/Include”,
“[0-9]{2}.[0-9].[0-9]{5}.[0-9]”,
],
“trim string”)
}
}
六、结语
这篇文章讲到这里,使用GN已经不在话下,但是要真正做到无惑,则和GN无关,因为编译器选项不是跨平台工具要解决问题的范畴,文章开头也有做说明。在./gn/BUILD.gn和./gn/BUILDCONFIG.gn这两个文件中,有大量编译器相关的选项设置,这些选项设置由于各平台编译器的差异,导致千差万别,非常晦涩,包括我自己也只了解其中一小部分。幸运的是,这部分内容我们有大量的时间去慢慢熟悉它,我们也无需记住每一项,其中99%以上设置项是我们也许永远不会关注到的,我相信经过实际工作开发后,有那么一小部分选项我们会接触到,并去调整它,届时我们只需要掌握那一部分就可以了。