什么Modern CMake,不存在的!
上手了一个多月的gn,感觉从玩具CMake终于到了Pro工具
官方文档很好查,就是说如果官方文档里没有,多半在其他地方也找不到用法。。
Part1 语法Intro
基础:手打gcc参数编译,手打ld链接(或了解其过程,或通过gcc调用linker)
入门照着这个做 https://gn.googlesource.com/gn/+/HEAD/docs/quick_start.md
- Config 入口在://.gn (包括编译器配置等)
- Target 入口在://BUILD.gn
其他的一切BUILD.gn还有gni等等都是由这两个展开引出的.
简单来说就是,先定义编译器和辅助工具例如时间戳stamp,再定义编译时候的默认参数,最后写一个自己的target: executable("hello") 就可以了
重点
- config字段指的是给编译器的传参,例如
-Wl,rpath=xx
, - config是 面向编译器的,最小的 参数集合,以单个config为单位可叠加/增删
- deps字段常用于target之间的依赖
Part2 进阶 及跨平台经验
再讲下我的设计思路,把编译配置文件分为了几部分
- 全局args:例如
build_type
,is_llvm
- 编译器:
toolchain("xxx"){}
- 编译器默认参数:
config("default_ldconfig"){}
- 插件:一些文件例如sanitizer.gni, imported_library.gni, protoc.gni,每个插件内部也有自己的控制变量
- Target:包括自己写的
executable(){}
,shared_library(){}
,static_library(){}
- windows下的cp命令我用批处理cp2.bat将就了一下
- 我把编译场景分为三块:linux, windows, 生产环境, 于是我根据这三个去写默认编译参数
//gn_build/toolchain_args/xxx
gni我理解成插件
- 这点chrome sourcecode里面很乱 一看就是历史包袱 改不得!
- 但是新工程我们要分清职责,一种方法是:把划分依据讲给自己听,到底是不是个模块划分,低耦合。
- 既然是插件就是可有可没的东西,依赖关系要清晰,如果不能(就在封装一层 《别)
- 例如sanitizer要加参数,按我的理解就是:sanitizer.gni里写了所有编译器的参数
第三方库 为了和别人协同开发方便,我都放在third文件夹下了,然后通过group()暴露给用户
- chrome 是模块都自带源码,通过重新编译做跨平台。。
- 我想简易跨平台,所以把每个平台的依赖编译好,分别放进平台文件夹例如linux_release, windows_release,合作者按需下载,每个跨平台的“第三方库集合”暴露一个build.gn供外部使用
- 又因为自己写的代码里不想有平台相关,所以在third下又中继了一个BUILD.gn
- 我把外部库理解成(假装)通过源码编译的库(target),而不是引入了加给编译器的参数(config),所以我就用group表示,引入时候就使用“deps=[xxx]”这种语法
- 另外一种做法是 按照第三方库名字放文件夹例如
/third/spdlog/BUILD.gn
,如果团队大部分人都要多平台支持,那最好使用这种(反正也不会节约空间)
关于Win32和Win64
- win32和win64是两种win(不同操作系统 可以并列)
- win64编译win32是cross build
- win64 运行win32程序 不是native的,理解成Linux下的WINE
其他
- 优化需要-march匹配目标cpu的指令集,也就是third文件夹都是要尽量从源码重新编译
- Windows下
StartMenu -> Visual Studio 2019 -> x64 Native Tools Command Prompt for VS 2019
就有cl可用了 - 查看属性,路径为"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
- gn生成的vs.sln不能在里面直接改nativeDebug/nativeRelease/amd3950Release (vs的debugger真好用,哎真是两难!)
希望有人扩展下gn - vscode下需要插件CodeLLDB用来调试(但需要手动添加target,建议谁撸个vscode extension
参数变量举例
假设我们现在有如下目录格式:
root
|--gn_build
|--(toolset and args...)
|--app
|--hello_world
|--sub
|--null.cpp
|--BUILD.gn
|--hello_world.cpp
|--sumfunc.cpp
|--sumfunc.h
|--out
|--win
|--(output here)
在root下执行
gn gen out/win
ninja -C out hello_world
举例分析app/hello_world/BUILD.gn
中
shared_library("sumfunc") {
sources = ["sumfunc.cpp", "sumfunc.h", "sub/null.cpp"]
}
executable("hello_world") {
sources = ["hello.cpp"]
deps = [":sumfunc"]
}
tool("cxx")
中使用的变量
tool("cxx") {
precompiled_header_type = "msvc"
depsformat = "msvc"
description = "CXX {{output}}"
outputs = [
"{{source_out_dir}}/{{label_name}}.{{source_name_part}}.obj",
]
# The PDB name needs to be different between C and C++ compiled files.
command = "cl.exe /nologo /showIncludes {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} /c {{source}} /Fo{{output}} /Fd\"{{target_out_dir}}/{{label_name}}_cc.pdb\""
}
针对上述hello_world涉及的3个cpp
{{output_dir}}
编译器运行目录cwd: out/win{{label_name}}
为target名:sumfunc
和hello_world
{{source_out_dir}}
分别为.cpp文件相对cwd路径:obj/app/hello_world
和obj/app/hello_world/sub
不知为啥该变量不能用在command中,只能用在outputs数组中,{{source_name_part}}
分别为文件名:sumfunc
,null
,hello_world
{{target_out_dir}}
均为编译的临时obj输出目录:obj/app/hello_world
该目录取自BUILD.gn所在文件夹{{output}}
其实是提示信息,即outputs数组,应该用在description里,用在command里有些tricky,因为不知为啥source_out_dir不能识别- 规律: {{source_xxx}} 均指.cpp的源文件所在地
- 规律: {{target_xxx}} 均指BUILD.gn所在地 和 BUILD.gn内的target名字
常见使用误区
static_lib和source_set的区别
在编译dll时 linker有一参数 (toolchain{tool("solink")}
部分)
--whole-archive
将static lib的代码带入自己的dll--no-whole-archive
不带入static lib的代码进入dll 即dll使用的symbol都是undefined的
如果exe->.so->.lib间接依赖,同时exe->.lib直接依赖,且lib代码代入.so (编译时使用--whole-archive
),此时exe内会有两份.lib的定义,违反ODR。
若使用source_set(),则不被认为是.lib,进而编译.so时一定会带入这部分代码,所以一定会违反ODR原则。
所以解决方案是:对于static lib和shared lib混编的程序,上述toolchain一定使用-Wl,--no-whole-archive
不让dll包含静态库内容,且独立且重复使用部分应使用static_lib而不是source_set。
注意:linker不会主动检查ODR
Upd 2023.03 一个完整的gn工具链开发环境
用了很久,基本问题都可以解决(包括解决互相引用)
待填坑,整理一下发Github