[2026-10-14] eagle: work/projects/vscode-migration/tech-design-xcode-build-server.md work/projects/vscode-migration/vscode-migration.md work/projects/vscode-migration/xcode-build-server.md
This commit is contained in:
1 parent
56b800e927
commit
cb6f8329a9
3 files changed
+169
No files matched your search
@@ -0,0 +1,107 @@
|
||||
# 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.
|
||||
@@ -6,6 +6,7 @@ Status: **IN PROGRESS** · updated 2026-10-04
|
||||
**Repo:** `~/DuckDuckGo/apple-browsers.git` (bare + worktrees)
|
||||
**Findings:** [[projects/vscode-migration/findings]]
|
||||
**Tech design (package step):** [[projects/vscode-migration/tech-design-spm-browser-package]]
|
||||
**Editor via Xcode build server (no package needed):** [[projects/vscode-migration/xcode-build-server]] · [[projects/vscode-migration/tech-design-xcode-build-server|tech design]]
|
||||
**Next:** [[projects/vscode-migration/modularization-roadmap]] — modularization after the browser package (incl. iOS/macOS sharing)
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# VS Code via Xcode build server (sourcekit-xcode-bsp)
|
||||
|
||||
Status: **prototype done, uncommitted** · 2026-10-14
|
||||
**Parent:** [[projects/vscode-migration/vscode-migration]]
|
||||
**Tech design:** [[projects/vscode-migration/tech-design-xcode-build-server]]
|
||||
|
||||
An alternative to the [[projects/vscode-migration/tech-design-spm-browser-package|SwiftPM browser package]]: keep the Xcode projects as they are and give VS Code/Cursor what it needs from them.
|
||||
- Code intelligence: sourcekit-lsp ← `buildServer.json` ← [sourcekit-xcode-bsp](https://github.com/slime-studio/sourcekit-xcode-bsp) (BSP server on top of swift-build, Xcode's build engine; early-stage, Apache 2.0).
|
||||
- Build/test/debug: `xcodebuild` tasks + lldb-dap launch configs.
|
||||
|
||||
## Where things are
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| VS Code setup | worktree `vscode-xcode-bsp`, branch `alex/vscode-xcode-bsp` — `DuckDuckGo.code-workspace`, `scripts/vscode/` (uncommitted) |
|
||||
| BSP fork | `~/Developer/sourcekit-xcode-bsp`, branch `configuration-name` (uncommitted), base upstream `726ce70`; same changes as `scripts/vscode/sourcekit-xcode-bsp.patch` |
|
||||
| Installed BSP | `~/.local/opt/sourcekit-xcode-bsp` (by `install-build-server.sh`); older dev copy in `~/.local/opt/sourcekit-xcode-bsp-dev`; brew 0.1.0 also installed |
|
||||
| Experiment leftovers | worktrees `cas-experiment-b/-c/-d` (detached), `~/Library/Caches/ddg-cas-exp` (CAS), `~/Library/Caches/ddg-spm-exp` (SPM checkouts) — delete |
|
||||
|
||||
## Files (`scripts/vscode/`)
|
||||
|
||||
- `setup-build-server.py [macos|ios]` — writes `buildServer.json`; runs on folder open. Target filter: app project + `*Tests-product`, no App Store variants, no other-platform app/local packages (from `iOS|macOS/LocalPackages`).
|
||||
- `sourcekit-xcode-bsp.sh` — `argv` of buildServer.json; editor's server kills a running background indexer (pid files in `.build/bsp/`, checks the pid is a sourcekit process).
|
||||
- `xcodebuild.sh <build|test> <scheme> [destination]` — Debug into `.build/xcodebuild`, shared CAS with `/^wt` `/^dd` prefix maps, xcbeautify.
|
||||
- `index-in-background.py [--all] [--if-idle]` — headless sourcekit-lsp indexing; `background-indexing.sh install` = launchd agent every 30 min.
|
||||
- `install-build-server.sh` + `sourcekit-xcode-bsp.patch` — build the patched BSP.
|
||||
|
||||
## Findings
|
||||
|
||||
1. **Works out of the box for app code**, Package.swift not needed. Swift extension 2.16.7 activates on `buildServer.json`.
|
||||
2. **Tests had no code intelligence.** BSP didn't set a configuration → swift-build used `defaultConfigurationName = Release` → no `ENABLE_TESTABILITY` (+ `-O`) → `@testable import DuckDuckGo_Privacy_Browser` fails → sourcekitd returns nothing. Fix: `configuration: Debug` (patch).
|
||||
3. **Stray `.o` in the repo root, linker failures, thousands of "duplicate output file".** BSP configured all 977 workspace targets in one build request: DMG + App Store variants, iOS + macOS, and each package library 3–4 times (`X` ×2, `X-product`, `Xdynamic-product`). They build into the same paths; swift-build drops tasks. Xcode never does this (scheme = one variant; package targets come as dependencies). Fix: `targets` include/exclude filter (patch) → 86 roots, 288 targets in the graph, 0 failures, 0 `.o`.
|
||||
4. **Indexing took ~2 h (estimate).** swift-build computes `textDocument/sourceKitOptions` per file, ~0.8 s each, single-threaded in `SWBBuildServiceBundle` (100% of one core), sending the whole build request every time. Swift args of a target are identical except the trailing `-index-unit-output-path` (= `outputPath` from `buildTarget/sources`). Fix: per-target cache + single flight (patch). **Full index of a fresh worktree: ~5.5 min** (~1 min workspace load + package resolve, ~3.5 min prepare+index, up to 11 parallel swift-frontend on M4 Max).
|
||||
5. **Critical path now = preparation**: ~250 prepare builds, serialized by `SWBBuildServer.preparationQueue` (~3 min).
|
||||
6. **Shared CAS between worktrees (xcodebuild):** works only with path mapping. Needed: `SWIFT/CLANG_ENABLE_PREFIX_MAPPING` + `*_OTHER_PREFIX_MAPPINGS` = `<DerivedData>=/^dd <worktree>=/^wt` (`-working-directory` is the worktree root, `ModuleCache.noindex` etc. sit in DerivedData). Clean build 179 s → **90–120 s, 0 cache misses** in a new worktree.
|
||||
- Xcode's own `*_ENABLE_PROJECT_PREFIX_MAPPING` maps each project/package dir to `/^src` — ambiguous, lldb can't find sources. Our `/^wt` + `sourceMap` in launch configs: breakpoints by full path bind, sources show.
|
||||
- Without mapping: B build 220 s, slower than cold (all SwiftCompile/CompileC miss).
|
||||
7. **CAS doesn't help the language server**: prepare-for-index doesn't use compilation caching (empty CAS stayed 60 KB; cold vs warm 347 s vs 344 s). Index units contain real paths (no `/^wt`).
|
||||
8. Indexes can't be shared between worktrees cheaply: absolute paths (sourcekit-lsp has `index.indexPrefixMap`) **and** sourcekit-lsp compares unit time with file mtime — fresh checkout = everything newer → full reindex. Would need mtime tricks; not pursued. The 5-min background index makes it unnecessary.
|
||||
9. `workspace/_pollIndex` isn't available in release sourcekit-lsp → indexer waits for `$/progress` "Indexing" end + 60 s quiet.
|
||||
10. swift-build service runs in its own process group: killing the BSP leaves `SWBBuildServiceBundle` orphaned → kill children first.
|
||||
11. Unrelated: main's `SharedPackages/BrowserServicesKit/Package.resolved` is stale (C-S-S 17.20 vs `exact: 17.26.0`) — every xcodebuild/Xcode resolve rewrites it. Spawned a separate task.
|
||||
|
||||
## Verified (2026-10-14, Xcode 27.1)
|
||||
|
||||
- Hover / definition / completion / diagnostics: macOS app file, macOS unit test (`@testable`, jumps into app source), package test, iOS app file (ios mode). Cross-module definition goes to source, not `.swiftinterface`, once indexed.
|
||||
- `xcodebuild.sh build "macOS Browser"`; lldb launch with breakpoint by full path + source map.
|
||||
- Background indexer: full run, editor takeover, no orphan processes, `--all` picks only configured worktrees.
|
||||
- BSP: 50 tests pass (added `TargetFilterTests`, `CachingMessageHandlerTests`), SwiftLint clean for new code.
|
||||
|
||||
Not verified: the VS Code UI itself (tasks, lldb-dap launch, problem matchers) — everything was driven headless.
|
||||
|
||||
## Open items
|
||||
|
||||
- [ ] Try it in VS Code/Cursor UI end to end.
|
||||
- [ ] Upstream PRs to slime-studio/sourcekit-xcode-bsp (configuration, targets + list-targets, buildSettings, arguments cache) — public repo, needs OK.
|
||||
- [ ] Upstream issue: stray `.o` / duplicate outputs when all targets are configured; BSP doesn't exit on stdin EOF while busy.
|
||||
- [ ] Decide team distribution: patch in repo (now) vs DDG fork + brew tap.
|
||||
- [ ] iOS debugging on simulator (simctl install/launch --wait-for-debugger + attach) — not done.
|
||||
- [ ] Clean up experiment worktrees and caches.
|
||||
- [ ] PR (needs Asana task link).
|
||||
Reference in new issue
Block a user