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

6.7 KiB
Raw Blame History

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: 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).