6.7 KiB
6.7 KiB
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: 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 (BSP server on top of swift-build, Xcode's build engine; early-stage, Apache 2.0). - Build/test/debug:
xcodebuildtasks + 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]— writesbuildServer.json; runs on folder open. Target filter: app project +*Tests-product, no App Store variants, no other-platform app/local packages (fromiOS|macOS/LocalPackages).sourcekit-xcode-bsp.sh—argvof 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/^ddprefix 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
- Works out of the box for app code, Package.swift not needed. Swift extension 2.16.7 activates on
buildServer.json. - Tests had no code intelligence. BSP didn't set a configuration → swift-build used
defaultConfigurationName = Release→ noENABLE_TESTABILITY(+-O) →@testable import DuckDuckGo_Privacy_Browserfails → sourcekitd returns nothing. Fix:configuration: Debug(patch). - Stray
.oin 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:targetsinclude/exclude filter (patch) → 86 roots, 288 targets in the graph, 0 failures, 0.o. - Indexing took ~2 h (estimate). swift-build computes
textDocument/sourceKitOptionsper file, ~0.8 s each, single-threaded inSWBBuildServiceBundle(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(=outputPathfrombuildTarget/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). - Critical path now = preparation: ~250 prepare builds, serialized by
SWBBuildServer.preparationQueue(~3 min). - Shared CAS between worktrees (xcodebuild): works only with path mapping. Needed:
SWIFT/CLANG_ENABLE_PREFIX_MAPPING+*_OTHER_PREFIX_MAPPINGS=<DerivedData>=/^dd <worktree>=/^wt(-working-directoryis the worktree root,ModuleCache.noindexetc. sit in DerivedData). Clean build 179 s → 90–120 s, 0 cache misses in a new worktree.- Xcode's own
*_ENABLE_PROJECT_PREFIX_MAPPINGmaps each project/package dir to/^src— ambiguous, lldb can't find sources. Our/^wt+sourceMapin launch configs: breakpoints by full path bind, sources show. - Without mapping: B build 220 s, slower than cold (all SwiftCompile/CompileC miss).
- Xcode's own
- 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). - 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. workspace/_pollIndexisn't available in release sourcekit-lsp → indexer waits for$/progress"Indexing" end + 60 s quiet.- swift-build service runs in its own process group: killing the BSP leaves
SWBBuildServiceBundleorphaned → kill children first. - Unrelated: main's
SharedPackages/BrowserServicesKit/Package.resolvedis stale (C-S-S 17.20 vsexact: 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,
--allpicks 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).