# 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`); brew 0.1.0 also installed | ## 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 [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` = `=/^dd =/^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. - [x] Clean up experiment worktrees and caches (2026-10-14). - [ ] PR (needs Asana task link).