用VSCode基于Bazel打造Apple生态开发环境
本期作者
张忻正
哔哩哔哩技术专家
引言
最近AIGC的爆发引发了非常多行业的恐慌也包括程序员群体。如何掌握工具例如Copilot等是下一个时代最重要的能力。但是在当前苹果封闭的生态下,对于先进的生产力的渴望也是促使这篇文章诞生的原因。
Xcode是苹果的用于研发苹果软件生态的集成研发环境(IDE)相信作为苹果最强生态的iOS研发应该完全不陌生。这个让人又熟悉又陌生的老兄弟跟着iOS的辉煌也已经快20岁了,笔者差不多是从3.x年代开始使用Xcode研发iOS 3.x的而这15年唯一不变的可能就是在商店里糟糕的评分和社区中不怎么好的口碑。
吐槽归吐槽,那让我们简单分析一下为什么我们心中的信仰会造出这样一个"不尽人意的产品"呢?我认为最重要的还是因为在工业界高强度的使用的我们并不是Xcode的目标人群。Xcode的主要服务目标还是大量的个人开发者,我相信这也是为什么Xcode会选择人类无法阅读的pbxproj的格式作为工程文件的重要原因。比起可读性和合并的方便性,个人开发者的虚拟文件结构,适合UE的高效处理的格式才是最重要的,同时也为每年的WWDCScholars (https://www.wwdcscholars.com/)们创造了舞台。我相信这也是苹果这家伟大企业的迷人所在。这里我也推荐个B站的小UP主(https://space.bilibili.com/456606920),我们可以看到也就Swift playgrounds这样的产品可以做到这么可爱的小朋友来教课吧。
回到正题,信仰依然是信仰只是信仰选择的服务面对我们这样在工业界摸爬滚打的社畜不那么友好(笑。解决这些问题感觉也逐渐变成了苹果生态筛选App质量的一个考题,接下来让我们好好刨析解构一下。
Xcode刨析
Features
既然我们要打造一个Apple生态的开发环境,那必须我们要钻研透目前的主要使用的Xcode,那么首先我们先要系统化的解构一下Xcode到底有什么?篇幅有限,我们今天就以Editor,Compiler, Debugger 三块来展开 Xcode - Features - Apple Developer(https://developer.apple.com/xcode/features/)
编辑器
编辑器作为整个IDE最大的显示区域,自然也是我们日常工作中最重最多接触的地方,当然这也是CLI GUI的战争前线。Code作为研发的工作量交付,自然怎么更好的Coding成为了编辑器是否好用的重中之重。我们可以看到包括Fix-it,Live Issues,Quick Help,Complete Documentation 都是用来帮助研发尽可能在同一个窗口中完成Coding的工作。关于Assistant Editor我推荐一篇文章帮助进阶使用Mastering the assistant editor in Xcode 11 - SwiftLee(https://www.avanderlee.com/xcode/xcode-assistant-editor/)。
至于非直接的Code交付的Interface Builder 和 Asset Catalog这里就又有一场与苹果理念的碰撞了。首先还是IB,业界其实有共识IB并不是一种易协作的文件结构,同时再与苹果自身的基于segue的拖拽理念,导致使用IB来描述逻辑和UI在大规模协作时已成为了灾难。相信不少同行应该是有规定禁止使用IB来实现UI的吧- -。然后说到Asset Catalog诚然xcrun有一整套actool的工具,但是让我们继续回到工业界,naming空间的冲突,基于字符串的索引都是灾难,这也导致了借鉴隔壁Android的 GitHub - mac-cain13/R.swift: Strong typed, autocompleted resources like images, fonts and segues in Swift projects (https://github.com/mac-cain13/R.swift)的产生。
编译器
无敌的LLVM,把多少c,c++,包括自己的Objective-C解救与水火之中。让造一门语言变得如此丝滑,也在此基础上孵化出了Swift。然而回到Xcode所有刚才叠的buff无一都在为编译器团队在诉苦。例如 针对子目录的无限递归添加search path,例如针对环境变量的SPM潜规则Flag,例如索引和编译共享的index store,每一项都是为了易用性对编译器团队的重拳出击。
让我们回到刚才编辑器中的与编译相关的AutoComplete的部分,结合起来灾难产生了。这也是Xcode的体验最让人吐槽的地方。
为了易用性的全量的索性 -> 巨慢的Indexing,以B站项目举例,5分钟卡不动,10分钟可以动鼠标
为了研发友好的大量暴力的编译Flag -> 巨差的工程体验,大量基于巧合,凑巧,顺序带来的恰好Work
编译与编辑的高度结合 -> 不做不错,越做越错,清理Derived Data成为常态,遇事不决先Clean
调试器
出色的OpenGL Frame捕捉能力,结合lldb,无论在速度,可用性上都是Top的存在。我们可以看到调试器就不用关心编辑器那个拖油瓶,只需要和编译器交互好就有着非常棒的体验,从而成为Xcode中满意度最高的一个环节。
B站的方法思路
尽量只使用Code本身,其他Xcode自带的例如IB等能力能不用则不同
回归编译本身,尽可能多的关闭xcodebuild自带的feature
全面拥抱Xcode的衍生研发工具链
业界方案
方案类型
从以上的刨析我们其实可以看到Xcode的每一项能力其实都是比较出色的,但是苦于没有一个较好的工程组织方式,把所有的能力合理组织起来。因此我们归类下主流的解决方案可以分为以下4种。其中最主流的做法是另发明一种工程组织模式的手段,从而借助此来生成并配置合理的pbxproj。
Xcode (标准)
Xcode + xcodeproj tools (tuist / XcodeGen / struct / rules_xcodeproj (bwx))
CocoaPods.org(https://cocoapods.org) GitHub - tuist/tuist: 🚀 Create, maintain, and interact with Xcode projects at scale(https://github.com/tuist/tuist) GitHub - yonaskolb/XcodeGen: A Swift command line tool for generating your Xcode project(https://github.com/yonaskolb/XcodeGen) GitHub - lyptt/struct: Xcode projects on steroids(https://github.com/lyptt/struct) GitHub - buildbuddy-io/rules_xcodeproj: Bazel rules for generating Xcode projects.(https://github.com/buildbuddy-io/rules_xcodeproj)
Xcode + other compile system (tulsi(bazel) / rules_xcodeproj(bwb) / cmake)
Tulsi - Tulsi(https://tulsi.bazel.build) Xcode — CMake 3.25.1 Documentation(https://cmake.org/cmake/help/latest/generator/Xcode.html?highlight=xcode)
VSCode + SPM (swift package manager)
https://github.com/swift-server/vscode-swift
Xcodebuild vs Bazel
我们的判断是基于xcodeproj本身是不可行的,这也是为什么会选择Bazel的原因,然而这个举动其实是风险和收益并存的,既然这样我们就需要足够强的论据。Bazel的设计原理可以使其增量编译(incremental build)出奇的快,那么在这里我们就对比一下Bazel相对差的Clean build,来进行一下对比。
测试项目: 使用哔哩哔哩的App工程项目
测试方式:
Xcode 通过rules_xcodeproj bwx 模式生成xcodebuild 项目
bazel run <TARGET>.xcodeproj
bazel clean --expunge
bazel shutdown
START
xcodebuild -scheme <TARGET>.scheme build
END
Bazel
bazel clean --expunge
bazel shutdown
START
bazel build <TARGET> --action_env=v=1
END
Bazel with rbe
分布式环境: 3.2 GHz 六核Intel Core i7 (2018) 16G * 20
由于内部已经在运行分布式系统,把整个系统重置代价过大,所以会通过action_env 的尽可能进行rebuild
bazel clean --expunge
bazel shutdown
START
bazel build--config=rbe --action_env=v=2
END
备注: 由于App项目结构巨大,并没有以多次取平均来测试,只用了一次结果作为数据参考
本机环境: Mac mini (M1, 2020) , Apple M1, 16GB, 12.5.1,有线网络
Xcode: 14.1
Bazel: 6.1.1
LOC(Line of code):
不含第三方包及工具链(External)代码
Objc, 4.5M
Swift, 500K
C++ & C & Objective-C++ 800K
测试结果
横向对比
我们简单把这4种类型的方案在大型项目(百万行以上)做个横向对比。
坚持在大型项目使用标准原生方案真的存在嘛?
Xcode + xcodeproj generator 方案是目前业界的主流,但是为了防止过大的项目的编译速度和研发体验,多少也都通过各种定制的sync手段局部的在binary和source之间切换
Xcode + other compile system 几乎都只用到了Xcode的Editor的能力,无疑这块的体验基本都是弱于原生的xcodeproj的(无论是否通过generator方案),周边设施,例如Debugger instruments等依然可以复用生态
这里要特别强调 rules_xcodeproj 的bwb(build with bazel)模式由于方案中不再使用任何xcode原生理解索引的方式,通过 GitHub - MobileNativeFoundation/index-import: Tool to import swiftc and clang index-store files into Xcode(https://github.com/MobileNativeFoundation/index-import)来实现更高效的索引,导致这个方案在Editor部分完全超过了原生的xcodeproj
VSCode + SPM 有着Editor环节下最好的研发体验,但是在周边设施,例如Test,Debugger方面缺异常差
综上我们挑出几个最常用的及我们的自研方案来横向对比
原生Xcode & Xcode + xcodeproj tools (例如CocoaPods)在此处默认为使用了build settings中的大部分default features,在全部关闭后各项体验都会大幅度提升
现阶段Bazel体系广泛使用的Xcode + tulsi
tulsi的替代品, Xcode + rules_xcodeproj (bwb mode)
VSCode + rules_bis (本次介绍的自研解决方案)
B站方案选型
B站目前的选型是大多数项目,基于Xcode + Bazel (tulsi)的方案,极少项目使用了Xcode + xcodeproj tools (rules_xcodeproj bwx) 的模式推进的。做出这个选择的主要的考量依据为 1. 大型项目工程化协作 2. 编译速度。从而牺牲了Coding体验部分。我们来分析下为什么Xcode + Bazel (tulsi) 会在Coding体验上失分那么多?
Tulsi 是把工程结构伪装成Xcode,然而实际的构建系统是基于Bazel的。从而导致Xcode中大量编译与编辑联动的部分失效。
Bazel可以自定义 Code gen的逻辑,然而xcodeproj的能力无法还原编译顺序,从而导致大量错误的编译指令出现。
Xcode在针对大型项目本身就捉襟见肘,叠加了一套伪装的编译系统的工程结构,更无力保证各个组件的稳定性。
我们可以从Line的小伙伴中的这个视频中来窥见一二 https://www.bilibili.com/video/BV15m4y1c76o/?spm_id_from=333.337.search-card.all.click&vd_source=da552eb941d5c396feceb614d38d1eb2
于是在去年11月,我做了一个决定。我们必须要抛弃tulsi,全面使用自研rules_bis 的模式,也写下了rules_bis的第一行代码。当然过程中,由于rules_xcodeproj的bwb模式发布(在2月份的正式rc,全面替换tulsi)B站选择了双工具栈的模式,按研发喜好自行选择 rules_xcodeproj or rules_bis。
VSCode + Bis
前言
这是一套基于VSCode的解决方案,Bis 是配套的VSCode插件
bis - Visual Studio Marketplace(https://marketplace.visualstudio.com/items?itemName=zxz-moe.zxz-moe-bis)
bis 的意思为(Best in slot) 希望是iOS研发同学的毕业装备,Bis的icon是以牧师的白月光`祈福`为原型所创作的。(允许我再为考迪克献上美好的祝福🙄)
早在2018年,B站作为国内较早尝试Bazel作为iOS主力编译系统的团队,当时就有一些很困难的决策,其中之一就是选择Facebook的Buck 还是选择 Google的Bazel作为Monorepo的工具。只从技术角度来看当时,两者最大的区别就是前者使用了基于Atom的自研IDE,后者使用了Tulsi作为Xcode的工程翻译。随着当时的团队人力,以及对工具链的把控能力我们选择了后者,但是随着基于Tulsi的摸爬滚打,针对Xcode背后工具链部分的逐渐深入,我们时常会自嘲是Xcode的逆向团队,但是随着Bis我们终于迈出了当年不敢跨的那一步。
依赖
SourceKit-LSP
这是我们需要介绍的第一块拼图。GitHub - apple/sourcekit-lsp: Language Server Protocol implementation for Swift and C-based languages(https://github.com/apple/sourcekit-lsp) 在次之前我们先要介绍一下微软的项目LSP Official page for Language Server Protocol(https://microsoft.github.io/language-server-protocol/) 作为行业老大哥,微软定义了各种语言与编辑器之间的标准规范,真正做到了帮助了各种语言提供者及使自己的VSCode成为了IDE中的佼佼者。而SourceKit-lSP 则为苹果对LSP的实现。其中Swift部分为项目实现,C系家族则依赖了clangd来实现。
SK-LSP 一共有3种工作模式分别为
1. BuildServer
2. Swift Package Manager
3. CompilationDatabase
if let buildServer = BuildServerBuildSystem(projectRoot: rootPath, buildSetup: buildSetup) {
buildSystem = buildServer
} else if let swiftpm = SwiftPMWorkspace(url: rootUrl,
toolchainRegistry: toolchainRegistry,
buildSetup: buildSetup) {
buildSystem = swiftpm
} else {
buildSystem = CompilationDatabaseBuildSystem(projectRoot: rootPath)
}
BuildServer
BuildServer模式可以通过在根目录放一个 buildServer.json来启动
buildServer 是 基于类似LSP的 BSP协议的一个服务描述 Build Server Protocol · Protocol for IDEs and build tools to communicate about compile, run, test, debug and more.(https://build-server-protocol.github.io) 他是由Jetbrains主导的协议标准。我们可以通过这个项目 GitHub - SolaWing/xcode-build-server: a build server protocol implementation for integrate xcode with sourcekit-lsp(https://github.com/SolaWing/xcode-build-server)来看怎么使用。这个项目同时也介绍了如果把一个xcodebuild的项目嫁接到sk-lsp 中使用。
SPM
SPM 模式就非常好理解了,他通过根目录是否存在 Package.swift 来判断当前是否是一个SPM项目从而以SPM的模式启动
CompilationDatabase
相比以上两种模式,CompilationDatabase主要是用来服务独立的编译系统例如CMAKE。
他通过根目录是否存在 compile_commands.json 来判断是否基于此启动。
我们可以在这里查看他的Format Specification。Bis使用的也就是CompilationDatabase,我们之后会就如何从构建系统中生成对应的compile_commands.json 来着重阐述。
JSON Compilation Database Format Specification — Clang 16.0.0git documentation(https://clang.llvm.org/docs/JSONCompilationDatabase.html)
vscode-swift
这是我们要介绍的第二块拼图,同时他也是Bis插件的依赖项。上一节说到,我们有一个LSP server实现了LSP协议,那么在对应的Development Tool 是谁呢?是谁把VSCode中的用户操作(打开文件,点击Goto)来告诉SK-LSP 以及谁来分析返回的结果告诉VSCode呢?答案就是 GitHub - swift-server/vscode-swift: Visual Studio Code Extension for Swift(https://github.com/swift-server/vscode-swift)
vscode-swift 是Swift Server Workgroup 维护为非Apple平台提供Swift支持的插件。Swift.org - Swift Extension for Visual Studio Code (https://www.swift.org/blog/vscode-extension/)我们可以从介绍中看到他主要是服务SwiftPM项目,这也是目前研发纯Swift项目体验最好的解决方案之一。不过在这里我们需要他的仅仅是桥接和SK-LSP的管道,因为我们使用的模式是CompilationDatabase
simctl & ios-deploy & codelldb
这是我们要介绍的最后一块拼图,也是Bis插件的依赖项。在上面两节中,我们已经把编辑器需要的能力全都做完了,接下来我们需要的就是调试器。借助强大的xcrun工具链以及lldb,其实在这一步我们并不需要做太多其他的事。其中的两个依赖项分别为
simctl & ios-deploy
模拟器管理 xcrun simctl
https://github.com/ios-control/ios-deploy
compile_commands.json
在我们上文介绍SourceKit-LSP 中着重提到了三种启动模式,而我们选择的是基于CompilationDatabase,而其中重中之重就是如何把compile_commands.json 给正确的解析出来。在这里我们借助了bazel aquery (Action Graph Query) https://bazel.build/query/aquery 的能力可以轻而易举的做到。让我们看一个典型的aquery例子。
bazel aquery 'mnemonic("SwiftCompile", (inputs("srcs/a/b/c/file.swift", deps(//srcs/target_a))))'
接下来我们就要分析aquery中的Inputs,Outputs,Arguments从而生成对应文件的编译指令了。其中会涉及把一些为了bazel缓存的占位符替换等逻辑。一个典型的产物如下。
{
"file": "srcs/base/x.swift",
"arguments": [
"swiftc",
"-target",
"x86_64-apple-ios11.0-simulator",
"-sdk",
"/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk",
"-emit-object",
"-output-file-map",
"bazel-out/ios-x86_64-min11.0-applebin_ios-ios_x86_64-dbg-ST-2954a897fcf1/bin/srcs/base/***.output_file_map.json",
"-Xfrontend",
"-no-clang-module-breadcrumbs",
"-emit-module-path",
"bazel-out/ios-x86_64-min11.0-applebin_ios-ios_x86_64-dbg-ST-2954a897fcf1/bin/srcs/base/***.swiftmodule",
"-enable-bare-slash-regex",
"-emit-objc-header-path",
"bazel-out/ios-x86_64-min11.0-applebin_ios-ios_x86_64-dbg-ST-2954a897fcf1/bin/srcs/***-Swift.h",
"-DDEBUG",
"-Onone",
"-Xfrontend",
"-no-serialize-debugging-options",
"-enable-testing",
"-g",
"-module-cache-path",
"bazel-out/ios-x86_64-min11.0-applebin_ios-ios_x86_64-dbg-ST-2954a897fcf1/bin/_swift_module_cache",
"-Xcc",
"-iquote.",
"-Xcc",
"-iquotebazel-out/ios-x86_64-min11.0-applebin_ios-ios_x86_64-dbg-ST-2954a897fcf1/bin",
"-Xfrontend",
"-color-diagnostics",
"-enable-batch-mode",
"-module-name",
"***",
"-parse-as-library",
"-warnings-as-errors",
"-static",
"-Xcc",
"-O0",
"-Xcc",
"-DDEBUG=1",
"-Xcc",
"-fstack-protector",
"-Xcc",
"-fstack-protector-all",
"-Xcc",
"-Wno-nullability-completeness",
"-Xcc",
"-Wno-#warnings",
"-Xcc",
"-Wno-deprecated-declarations",
"srcs/base/***.swift",
"srcs/base/***2.swift",
"srcs/base/***3.swift"
],
"directory": "/Users/**/Workspace/**"
}
至此一个文件的compile_commands.json 就生成完了,细心的同学一定会发现个疑惑,如果编译参数中有依赖其他的library,依赖其他的头文件怎么办?所以其实光靠一个静态的aquery是完全不够的,因为我们在正常的项目中一定存在依赖关系。这里依靠bazel强大的rules自定义能力,我们又一次可以轻而易举的通过aspects分析某个target下依赖的所有module(swift module,或者是C系module) bis/bisproject_aspect.bzl at main · xinzhengzhang/bis · GitHub (https://github.com/xinzhengzhang/bis/blob/main/bisproject_aspect.bzl)然后我们只需要把这些module正确编译出来放到aquery executor的 deps里就可以。
至此,一个完全通过当前编译系统产出的编译指令就完成了。
bazel run @bis//:setup -- --target //srcs/ios:App --file_path srcs/ios/app.swift
Bis
三块依赖拼图介绍完,我们先简单直观感受一下。
项目初始化 & UE
调试体验
Known issue
Swift module无法跳转 `import moduleA` 由于当前最新xcode内置的 sourcekit-lsp还是过老,可参见项目README自行编译对应版本
一些其他插件推荐
copilot: github.copilot
为了这碟醋包了这盘饺子
cursor: jtiays.aicursor
gpt4.0
gitlens: eamodio.gitlens
git flow
apple-swift-format: vknabel.vscode-apple-swift-format
swift format
Toggle Header/Source: bbenoist.togglehs
heder toggle
Reviews
名词解释: fastbuild 为B站体系内部牺牲正确性来换break change不重编的技术手段。
Final word
rules_bis本来的设计目标是iOS生态的插件(Swift, Objc, Cpp) 但是随着本文的提笔,chatgpt的爆发,让笔者相信这碟醋已经不是醋了,他是一朵产醋的泉眼,他是帮助Apple生态和AI打通的桥梁,所以我决定在0.3.0的版本进行了一次完整的重构,从面向iOS研发进化成了面向Apple多平台生态的插件。目前支持范围见下表,当然还有非常多Apple生态体系下未建设的平台例如watch,tv,linux (swift-server)等,有着rules_apple的支持我相信这些支持都不是什么难事,限于精力如果有兴趣欢迎参与共建 GitHub - xinzhengzhang/bis: Bazel rule for development in the Apple ecosystem through the C family(including swift) language(https://github.com/xinzhengzhang/bis)。在发文时,B站所有Apple项目已经切换至vscode(bis) / rules_xcodeproj(bwb) /双模式下
PS. 针对xctest 体系的XCTAutomation(即ios_ui_test) 的debug 方式如果有思路请务必联系我!(现阶段可以通过 rules_xcodeproj 来绕过)
References
Xcode - Features - Apple Developer(https://developer.apple.com/xcode/features/)
GitHub - bazelbuild/tulsi: An Xcode Project Generator For Bazel(https://github.com/bazelbuild/tulsi)
GitHub - MobileNativeFoundation/rules_xcodeproj: Bazel rules for generating Xcode projects.(https://github.com/MobileNativeFoundation/rules_xcodeproj)
GitHub - swift-server/vscode-swift: Visual Studio Code Extension for Swift(https://github.com/swift-server/vscode-swift)
Official page for Language Server Protocol(https://microsoft.github.io/language-server-protocol/)
GitHub - apple/sourcekit-lsp: Language Server Protocol implementation for Swift and C-based languages(https://github.com/apple/sourcekit-lsp)
JSON Compilation Database Format Specification — Clang 17.0.0git documentation(https://clang.llvm.org/docs/JSONCompilationDatabase.html)
GitHub - hedronvision/bazel-compile-commands-extractor: Goal: Enable awesome tooling for Bazel users of the C language family.(https://github.com/hedronvision/bazel-compile-commands-extractor)
GitHub - ios-control/ios-deploy: Install and debug iPhone apps from the command line, without using Xcode(https://github.com/ios-control/ios-deploy)
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路