用VSCode基於Bazel打造Apple生態開發環境
來源:嗶哩嗶哩技術
引言
最近AIGC的爆發引發了非常多行業的恐慌也包括程式設計師群體。如何掌握工具例如Copilot等是下一個時代最重要的能力。但是在當前蘋果封閉的生態下,對於先進的生產力的渴望也是促使這篇文章誕生的原因。
Xcode是蘋果的用於研發蘋果軟體生態的整合研發環境(IDE)相信作為蘋果最強生態的iOS研發應該完全不陌生。這個讓人又熟悉又陌生的老兄弟跟著iOS的輝煌也已經快20歲了,筆者差不多是從3.x年代開始使用Xcode研發iOS 3.x的而這15年唯一不變的可能就是在商店裡糟糕的評分和社群中不怎麼好的口碑。
吐槽歸吐槽,那讓我們簡單分析一下為什麼我們心中的信仰會造出這樣一個"不盡人意的產品"呢?我認為最重要的還是因為在工業界高強度的使用的我們並不是Xcode的目標人群。Xcode的主要服務目標還是大量的個人開發者,我相信這也是為什麼Xcode會選擇人類無法閱讀的pbxproj的格式作為工程檔案的重要原因。比起可讀性和合並的方便性,個人開發者的虛擬檔案結構,適合UE的高效處理的格式才是最重要的,同時也為每年的WWDCScholars ()們創造了舞臺。我相信這也是蘋果這家偉大企業的迷人所在。這裡我也推薦個B站的小UP主(),我們可以看到也就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()。
至於非直接的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 ()的產生。
編譯器
無敵的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() GitHub - tuist/tuist: ? Create, maintain, and interact with Xcode projects at scale() GitHub - yonaskolb/XcodeGen: A Swift command line tool for generating your Xcode project() GitHub - lyptt/struct: Xcode projects on steroids() GitHub - buildbuddy-io/rules_xcodeproj: Bazel rules for generating Xcode projects.()
Xcode + other compile system (tulsi(bazel) / rules_xcodeproj(bwb) / cmake)
Tulsi - Tulsi() Xcode — CMake 3.25.1 Documentation()
VSCode + SPM (swift package manager)
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()來實現更高效的索引,導致這個方案在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的小夥伴中的這個影片中來窺見一二
於是在去年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()
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() 在次之前我們先要介紹一下微軟的專案LSP Official page for 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.() 他是由Jetbrains主導的協議標準。我們可以透過這個專案 GitHub - SolaWing/xcode-build-server: a build server protocol implementation for integrate xcode with sourcekit-lsp()來看怎麼使用。這個專案同時也介紹瞭如果把一個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()
vscode-swift
這是我們要介紹的第二塊拼圖,同時他也是Bis外掛的依賴項。上一節說到,我們有一個LSP server實現了LSP協議,那麼在對應的Development Tool 是誰呢?是誰把VSCode中的使用者操作(開啟檔案,點選Goto)來告訴SK-LSP 以及誰來分析返回的結果告訴VSCode呢?答案就是 GitHub - swift-server/vscode-swift: Visual Studio Code Extension for 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
compile_commands.json
在我們上文介紹SourceKit-LSP 中著重提到了三種啟動模式,而我們選擇的是基於CompilationDatabase,而其中重中之重就是如何把compile_commands.json 給正確的解析出來。在這裡我們藉助了bazel aquery (Action Graph Query) 的能力可以輕而易舉的做到。讓我們看一個典型的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 ()然後我們只需要把這些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()。在發文時,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()
GitHub - MobileNativeFoundation/rules_xcodeproj: Bazel rules for generating Xcode projects.()
GitHub - swift-server/vscode-swift: Visual Studio Code Extension for Swift()
Official page for Language Server Protocol()
GitHub - apple/sourcekit-lsp: Language Server Protocol implementation for Swift and C-based languages()
JSON Compilation Database Format Specification — Clang 17.0.0git documentation()
GitHub - hedronvision/bazel-compile-commands-extractor: Goal: Enable awesome tooling for Bazel users of the C language family.()
GitHub - ios-control/ios-deploy: Install and debug iPhone apps from the command line, without using Xcode()
以上是今天的分享內容,如果你有什麼想法或疑問,歡迎大家在留言區與我們互動,如果喜歡本期內容的話,歡迎點個“在看”吧!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027824/viewspace-2951118/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於vscode搭建freertos環境VSCode
- 基於Gulp小程式開發工作流,區分開發環境和生產環境開發環境
- 用 vscode 配置 java 開發環境發 qq 郵件VSCodeJava開發環境
- Vscode配置golang開發環境VSCodeGolang開發環境
- VSCode Python開發環境配置VSCodePython開發環境
- VSCode配置JAVA開發環境VSCodeJava開發環境
- 用 Spring 區分開發環境、測試環境、生產環境Spring開發環境
- 編譯環境 Golang開發環境 vscode+git編譯Golang開發環境VSCodeGit
- 【Lua】VSCode 搭建 Lua 開發環境VSCode開發環境
- VSCode + Docker 的 PHP 開發環境VSCodeDockerPHP開發環境
- Flutter開發入門之開發環境搭建(VSCode搭建Flutter開發環境)Flutter開發環境VSCode
- 基於 Xcode 搭建 OpenCV 開發環境XCodeOpenCV開發環境
- 基於Docker的LNMP開發環境DockerLNMP開發環境
- 基於Webpack搭建React開發環境WebReact開發環境
- 基於 PAYJS 微信支付個人介面開發的 Laravel Package,可直接用於生產環境JSLaravelPackage
- VSCODE 配置 C/C++ 開發環境VSCodeC++開發環境
- Go語言VSCode開發環境配置GoVSCode開發環境
- VSCode for Mac 搭建 Common Lisp 開發環境VSCodeMacLisp開發環境
- VsCode配置C/C++開發環境VSCodeC++開發環境
- VSCode+Maven+Hadoop開發環境搭建VSCodeMavenHadoop開發環境
- OpenShift 本地開發環境配置(基於 Minishift)開發環境
- 基於 Vagrant 構建 PHP 開發環境PHP開發環境
- 基於滴滴雲搭建 Ceph 開發環境開發環境
- 搭建基於 Mac 的 Flutter 開發環境MacFlutter開發環境
- win10+vscode部署java開發環境Win10VSCodeJava開發環境
- docker 生產環境基礎應用Docker
- VsCode從零開始配置一個屬於自己的Vue開發環境VSCodeVue開發環境
- VSCode系列 - 如何用VSCode搭建C++高效開發環境(1)VSCodeC++開發環境
- VSCode系列 - 如何用VSCode搭建C++高效開發環境(2)VSCodeC++開發環境
- 基於 Webpack4 搭建 Vue 開發環境WebVue開發環境
- 如何構建基於 docker 的開發環境Docker開發環境
- 基於 docker 開發環境下-配置 PHPStorm xdebugDocker開發環境PHPORM
- 用前端姿勢玩docker【四】基於docker快速構建webpack的開發與生產環境前端DockerWeb
- webpack4-06-開發、生產環境、動態CDN配置Web
- Webpack(開發、生產環境配置)Web
- 基於 VSCode下的 Flutter 開發VSCodeFlutter
- 基於Docker搭建PHP+Nginx+MySQL開發環境DockerPHPNginxMySql開發環境
- 基於 Docker 構建統一的開發環境Docker開發環境