Files
obsidian-vault/work/projects/vscode-migration/tech-design-xcode-build-server.md
T

108 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<project>/<target>`, 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.