108 lines
9.2 KiB
Markdown
108 lines
9.2 KiB
Markdown
# 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.
|