9.2 KiB
9.2 KiB
Tech Design: VS Code and Cursor on the Xcode projects through a build server
Author: Alex M Reviewer: TBD Stakeholders: macOS and iOS teams Project: Migration to fully VSCode-supported builds&debugging · notes: projects/vscode-migration/xcode-build-server
Background & Requirements
- Developers increasingly work in VS Code and Cursor (AI agents, multiple worktrees), but code intelligence for our apps only exists in Xcode.
- The Swift extension's language server, sourcekit-lsp, understands SwiftPM packages natively. For other build systems it talks to a build server through the Build Server Protocol (BSP), configured by a
buildServer.jsonin the folder. - The current plan makes the macOS browser code a SwiftPM package (projects/vscode-migration/tech-design-spm-browser-package). That is a big change to the project, and macOS only.
- sourcekit-xcode-bsp (Apache 2.0, early-stage, version 0.1.0) is a build server for
.xcodeproj/.xcworkspace. It uses swift-build, the open-source build engine of Xcode, so it reads the projects the way Xcode does. - Requirements:
- Code intelligence (completion, diagnostics, jump to definition, references) for app, test and package code, macOS and iOS.
- Build, run, test and debug from the editor.
- No change to the Xcode projects, schemes, CI or release.
- Works in many worktrees at once without hours of indexing per worktree.
Problem Statement
Out of the box, sourcekit-xcode-bsp 0.1.0 on our workspace:
- gives app code intelligence after ~3 minutes;
- gives nothing in test files;
- produces build failures and drops ~250 object files into the repository root;
- needs an estimated 2 hours to index a worktree.
Recommended Approach
Use sourcekit-xcode-bsp with three changes to it, plus a small setup in the repository.
Changes to sourcekit-xcode-bsp (scripts/vscode/sourcekit-xcode-bsp.patch, to be upstreamed)
configuration, defaultDebug. Without it swift-build uses the projects' default configuration, Release, which has no testability. Every@testable importfailed, so test files had no results.targetsinclude/exclude patterns on<project>/<target>, plus alist-targetscommand to check them. The server put all 977 workspace targets into one build: DMG and App Store variants, both platforms, and 3–4 variants of each package library. They build into the same paths, so swift-build dropped tasks (failed links, stray.ofiles). Xcode never builds them together; a scheme picks one variant and package targets come in as dependencies. We configure the same way: 86 targets, which pull in 288.- A per-target cache of Swift compiler arguments. swift-build computes them for every file from scratch (~0.8 s each, one at a time). Within a Swift target they differ only in the file's
-index-unit-output-path, which the server already reports. The first request of a target is computed and the rest are answered from the cache. Indexing a fresh worktree went from ~2 hours to ~5.5 minutes on an M4 Max.
Also added: buildSettings overrides.
In the repository (DuckDuckGo.code-workspace, scripts/vscode/)
setup-build-server.py [macos|ios]writesbuildServer.jsonon folder open: Debug, one platform, and the target filter. The filter keeps the app project and package test products, and drops App Store variants and the other platform's app and local packages.- Tasks call
xcodebuild.sh: build a scheme (macOS or iOS Simulator) and run unit tests with-only-testing. Output goes through xcbeautify, so errors show in the Problems panel. - Launch configurations debug the app with lldb-dap: launch, launch without building, and attach.
- Compile results shared between worktrees.
xcodebuild.shuses Xcode's compilation cache with the worktree and build folder mapped to/^wtand/^dd. A clean build in a new worktree drops from 179 s to 90–120 s, with no cache misses. Launch configurations map/^wtback withsourceMap, so breakpoints and sources work. - Background indexing.
index-in-background.pyindexes worktrees headless while no editor uses them, and an editor that opens stops it.background-indexing.sh installruns it every 30 minutes while Xcode, VS Code and Cursor are closed (opt-in). - Install.
install-build-server.shbuilds the patched server from a pinned upstream commit into~/.local/opt.
Steps
- Land the setup and the patch in the repository, behind no flags; Xcode users aren't affected.
- Open upstream PRs for the three changes; drop the patch when a release has them.
- iOS simulator debugging (install, launch waiting for the debugger, attach), as a follow-up.
Pros
- No project changes. The Xcode projects, schemes and CI stay the source of truth; nothing to keep in sync.
- Same build settings as Xcode. swift-build plans the build, so flags, macros, plugins, resources and per-configuration settings match Xcode. Hand-written SwiftPM manifests can drift from them.
- Both platforms and every target type: apps, extensions, app tests, package tests.
- Fast enough for many worktrees: ~5.5 min first index, background indexing, shared compile cache for builds.
- Complements the SwiftPM package work rather than replacing it: modularization still helps build times and testing.
Cons
- A young third-party tool (0.1.0, "early-stage" by its authors) and our patch on top until upstream merges it.
- One platform per worktree at a time; switching runs the setup again and reindexes.
- Files outside the configured targets have no results, for example App Store-only files (none today) or packages only the other platform uses.
- First index of a worktree is ~5.5 minutes, and preparation is serialized inside swift-build.
- xcodebuild builds take as long as in Xcode; VS Code adds nothing there except the shared cache for new worktrees.
Risks
- swift-build changes with Xcode. The server bundles its own swift-build build, pinned by upstream, while the compiler comes from the selected Xcode. Mitigation: pin a tested commit in
install-build-server.sh; check after each Xcode update. - Upstream doesn't take the patches or the project stalls. Mitigation: keep a fork under the DuckDuckGo organization; the patch is ~350 lines plus tests.
- Prefix-mapped debug info (
/^wt): a debugger without the source map shows no sources. Mitigation: onlyxcodebuild.shbuilds use it, and the launch configurations setsourceMap. Xcode builds are unchanged. - Background indexing costs CPU and disk (~5 GB per worktree in
.build/). Mitigation: it's opt-in, runs only while editors are closed, at low priority, and skips worktrees that haven't changed.
Notes
- [1] Measured on an M4 Max with Xcode 27.1 on
mainat 3d850d74dc.- Full index of a fresh worktree: 323–347 s. That is ~1 min to load the workspace and resolve packages, then ~3.5 min to prepare and index, running up to 11 compilers in parallel.
- Before the arguments cache: 6,000 of ~8,000 index units after 48 min.
- [2] The compilation cache doesn't speed up the language server, because swift-build doesn't use it for index preparation. A cold and a warm cache gave the same time. So the build server config doesn't set it.
- [3] Sharing the index between worktrees was rejected. Index records hold absolute paths, and sourcekit-lsp reindexes any file newer than its record, which is every file in a fresh checkout.
- [4] Alternatives considered:
- xcode-build-server (parses Xcode build logs): needs a full Xcode build first and goes stale until the next build.
- SwiftPM package for the app code (current plan): the package work stays useful, but the editor setup doesn't have to wait for it.
- SweetPad extension: wraps xcodebuild and xcode-build-server; same staleness, plus another extension to maintain.
Testing
- Code intelligence checked with a headless LSP client (hover, definition, completion, a diagnostic for an injected type error) on:
- a macOS app file;
- a macOS unit test, including a jump into app code;
- a package test;
- an iOS app file in iOS mode.
xcodebuild.sh build "macOS Browser"builds. lldb stops at a breakpoint set by full path in the cache-mapped build, with sources shown.- Background indexer:
- full run;
- an editor taking over mid-run, with no orphaned processes;
- selecting only configured worktrees.
- sourcekit-xcode-bsp: 50 unit tests pass, including new tests for the target filter and the arguments cache (single flight, invalidation, stale results).
- To do before rollout: a pass through the VS Code and Cursor UI (tasks, Problems panel, lldb-dap launch and attach) by two people on fresh machines.
Additional Considerations
- Privacy / Security: No change to the apps. The install script downloads and builds sourcekit-xcode-bsp from its public repository at a pinned commit. Everything runs locally.
- Site Breakage / Experimentation: Not applicable.
- Operational: No CI or release change. Opting in means installing the server and opening the workspace file. Xcode users aren't affected.
- Localization: Not applicable.