# 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](https://app.asana.com/1/137249556945/project/1203301625297703/task/1209808486380252) ยท 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.json` in the folder. - The current plan makes the macOS browser code a SwiftPM package ([[projects/vscode-migration/tech-design-spm-browser-package|tech design]]). That is a big change to the project, and macOS only. - [sourcekit-xcode-bsp](https://github.com/slime-studio/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) 1. **`configuration`, default `Debug`.** Without it swift-build uses the projects' default configuration, Release, which has no testability. Every `@testable import` failed, so test files had no results. 2. **`targets` include/exclude patterns** on `/`, plus a `list-targets` command 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 `.o` files). 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. 3. **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]` writes `buildServer.json` on 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.sh` uses Xcode's compilation cache with the worktree and build folder mapped to `/^wt` and `/^dd`. A clean build in a new worktree drops from 179 s to 90โ€“120 s, with no cache misses. Launch configurations map `/^wt` back with `sourceMap`, so breakpoints and sources work. - **Background indexing.** `index-in-background.py` indexes worktrees headless while no editor uses them, and an editor that opens stops it. `background-indexing.sh install` runs it every 30 minutes while Xcode, VS Code and Cursor are closed (opt-in). - **Install.** `install-build-server.sh` builds the patched server from a pinned upstream commit into `~/.local/opt`. **Steps** 1. Land the setup and the patch in the repository, behind no flags; Xcode users aren't affected. 2. Open upstream PRs for the three changes; drop the patch when a release has them. 3. 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: only `xcodebuild.sh` builds use it, and the launch configurations set `sourceMap`. 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 `main` at 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.