跨平台: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平台编译入口脚本
./build_for_mac.sh,关键脚本如下(只是截取部分关键代码,旨在让大家看到各平台下gn编译命令的调用):
# 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:4221 {<!-- -->{arflags}} /OUT:{<!-- -->{output}} @</span><span class="katex-html"><span class="base"><span class="strut" style="height: 1em; vertical-align: -0.25em;"></span><span class="mord mathit" style="margin-right: 0.01968em;">l</span><span class="mord mathit">i</span><span class="mord mathit">b</span><span class="mord">/</span><span class="mord mathit">n</span><span class="mord mathit">o</span><span class="mord mathit" style="margin-right: 0.01968em;">l</span><span class="mord mathit">o</span><span class="mord mathit" style="margin-right: 0.03588em;">g</span><span class="mord mathit">o</span><span class="mord">/</span><span class="mord mathit">i</span><span class="mord mathit" style="margin-right: 0.03588em;">g</span><span class="mord mathit">n</span><span class="mord mathit">o</span><span class="mord mathit" style="margin-right: 0.02778em;">r</span><span class="mord mathit">e</span><span class="mspace" style="margin-right: 0.277778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right: 0.277778em;"></span></span><span class="base"><span class="strut" style="height: 1em; vertical-align: -0.25em;"></span><span class="mord">4</span><span class="mord">2</span><span class="mord">2</span><span class="mord">1</span><span class="mord"><span class="mord"><span class="mord mathit">a</span><span class="mord mathit" style="margin-right: 0.02778em;">r</span><span class="mord mathit" style="margin-right: 0.10764em;">f</span><span class="mord mathit" style="margin-right: 0.01968em;">l</span><span class="mord mathit">a</span><span class="mord mathit" style="margin-right: 0.03588em;">g</span><span class="mord mathit">s</span></span></span><span class="mord">/</span><span class="mord mathit" style="margin-right: 0.02778em;">O</span><span class="mord mathit" style="margin-right: 0.10903em;">U</span><span class="mord mathit" style="margin-right: 0.13889em;">T</span><span class="mspace" style="margin-right: 0.277778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right: 0.277778em;"></span></span><span class="base"><span class="strut" style="height: 0.88888em; vertical-align: -0.19444em;"></span><span class="mord"><span class="mord"><span class="mord mathit">o</span><span class="mord mathit">u</span><span class="mord mathit">t</span><span class="mord mathit">p</span><span class="mord mathit">u</span><span class="mord mathit">t</span></span></span><span class="mord">@</span></span></span></span></span>rspfile””<br> if (win_xp) {<!-- --><br> command += " /SUBSYSTEM:WINDOWS,5.01"<br> }</em></p>