# VS Code via Xcode build server (sourcekit-xcode-bsp) Status: **local setup deployed to all 97 registered worktrees; app-repository setup remains uncommitted** · 2026-10-16 **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` ← [our sourcekit-xcode-bsp fork](https://github.com/mallexxx/sourcekit-xcode-bsp) ([upstream](https://github.com/slime-studio/sourcekit-xcode-bsp); BSP server on top of swift-build, Xcode's build engine; early-stage 0.1.0, Apache 2.0). - Build/test/debug: `xcodebuild` tasks + lldb-dap launch configs, macOS and iOS simulator. - Builds and index shared with Xcode through the worktree's DerivedData folder. ## Where things are | What | Where | |---|---| | Original migration worktree | `/Users/admin/DuckDuckGo/apple-browsers.git/vscode-xcode-bsp`, branch `alex/vscode-xcode-bsp`; original uncommitted setup preserved | | Local bootstrap | `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py` | | Master templates | `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/template/` | | Active shared Git hook | `/Users/admin/DuckDuckGo/apple-browsers.git/.git/hooks/post-checkout` | | Local ignore rules | `/Users/admin/DuckDuckGo/apple-browsers.git/.git/info/exclude` | | Published BSP fork | [mallexxx/sourcekit-xcode-bsp](https://github.com/mallexxx/sourcekit-xcode-bsp), default branch `xcode-workspace-support`; pinned revision `afd0e3dd026b3ee3619e2271809debee25fec96f` | | Fork checkout | `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/sourcekit-xcode-bsp`; the earlier draft at `/Users/admin/Developer/sourcekit-xcode-bsp` was preserved | | Installed BSP binaries | `/Users/admin/.local/opt/sourcekit-xcode-bsp/`, shared by worktrees; existing patched installation was not replaced during the rollout | ## Local hook and all-worktree rollout — 2026-10-16 **Deployed and verified:** the real shared Git hook is active, and the setup has been applied to **all 97 registered Apple browser worktrees**, including the two checkouts beside `apple-browsers.git`. This is a local deployment on Alex's machine; no app-repository commit, PR, or upstream issue was created. ### Trigger and ownership Git's repository-local `core.hooksPath` is already `/Users/admin/DuckDuckGo/apple-browsers.git/.git/hooks`. Its executable `post-checkout` hook runs after a normal `git worktree add`, `git checkout`, or `git switch` with the branch-checkout flag. It finds that worktree's root and runs: ```sh python3 /Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py apply "$root" ``` The bootstrap accepts worktrees sharing this repository's Git common directory, including paths outside the bare-repository container. It copies central templates rather than linking worktrees to each other. It generates each worktree's absolute workspace/launcher paths and Xcode DerivedData location, preserves the selected macOS/iOS platform, and uses hashes to update files it owns while preserving custom edits and tracked files. A normal fresh-worktree creation was tested with the installed hook, without a temporary `core.hooksPath` override: all 17 files appeared automatically, were ignored, and left Git status clean. The hook itself runs no network requests, installer, build, or indexing job. Its failure is reported without failing checkout. Git does not run this hook for `worktree add --no-checkout`; a later checkout does. Tools that disable or bypass Git hooks need an explicit apply command. No cron, launchd job, or filesystem watcher was installed for bootstrapping. ### Files inside each worktree ```text / DuckDuckGo.code-workspace buildServer.json .vscode/ settings.json tasks.json launch.json extensions.json scripts/vscode/ .build/vscode-local/files.json ``` `files.json` records ownership hashes. Each newly bootstrapped worktree receives 17 setup files; existing custom files may be retained instead. Open that worktree's `DuckDuckGo.code-workspace` in VS Code/Cursor. The binaries remain shared at `/Users/admin/.local/opt/sourcekit-xcode-bsp/`. The shared local `.git/info/exclude` contains: ```gitignore /DuckDuckGo.code-workspace /scripts/vscode/ /.vscode/ /buildServer.json /.vscode-local/ ``` No tracked `.gitignore` was edited. Existing tracked files remain tracked despite these exclusions. ### Commands and rollback The following install and backfill commands have already been run: ```sh python3 /Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py install python3 /Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py apply --all ``` For an individual worktree or a preview: ```sh python3 /Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py apply /absolute/worktree/path python3 /Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/worktrees.py apply /absolute/worktree/path --dry-run ``` The installer refuses to replace an existing different hook. To disable future automatic runs, move the installed `post-checkout` file out of the hooks directory (for example to `.vscode-local/post-checkout.disabled`); existing generated files remain. There is no dedicated uninstall command. ### Rollout verification and legacy exceptions The backfill wrote 1,526 files across 97 worktrees and preserved 45 existing setup/editor-file entries. Structural verification found **zero failures**: expected files existed, server names and absolute paths matched each worktree, wrappers were executable, generated editor JSON parsed, and newly created files were Git-ignored. This was not a new app build, indexing run, or debugger test in every worktree. The old `xcode-build-server` configurations in `exp` and `sweetpad` were migrated after backing up their originals: - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backups/backfill-20261015T185058Z/exp/buildServer.json` - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backups/backfill-20261015T185058Z/sweetpad/buildServer.json` **Tracked-file exception:** the `exp` branch already tracks `buildServer.json`. Its migration is therefore an intentional **uncommitted local modification**, not an ignored file. All other previously tracked setup files were preserved unchanged. The hook skips tracked files, so a later branch checkout can restore that branch's tracked legacy config; the existing folder-open setup task regenerates the selected BSP config when opening the workspace. Evidence: - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backfill-report.json` - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backfill.log` - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backfill-before.json` - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/backfill-migrations.json` - `/Users/admin/DuckDuckGo/apple-browsers.git/.vscode-local/research.txt` ### Publishing scope The BSP fixes are public in the personal fork, with a committed dependency lockfile. All 58 tests and strict SwiftLint on the changed Swift files passed. The local template installer pins `afd0e3dd026b3ee3619e2271809debee25fec96f`, uses `xcrun swift` and `--force-resolved-versions`, and no longer applies a patch during installation. The old patch snapshot remains for compatibility with the installed fingerprint check; the original migration worktree's differing installer/README were preserved. The bootstrap is still specific to this project: workspace/project names, schemes, app paths and target filters are hardcoded. Its installer also assumes an existing absolute `core.hooksPath`. A general public bootstrap needs configuration, Git layout/hook handling, packaging, removal support, and tests against another Xcode project. A DuckDuckGo-owned fork, team rollout and upstream PRs remain separate work. ## Files (`scripts/vscode/`) - `setup-build-server.py [macos|ios]` — writes `buildServer.json` on folder open. Keeps the current platform without an argument. Build root = the worktree's Xcode DerivedData, `derivedDataLayout: xcode`, `configuration: Debug`, target filter (app project + `*Tests-product`, no App Store variants, no other-platform app/local packages). Warns when the installed BSP was built from an older patch. - `xcode_derived_data.py` — Xcode's DerivedData folder for the worktree (MD5 → 28 letters; honors a custom location). - `sourcekit-xcode-bsp.sh` — `argv` of buildServer.json. Editor's server stops a running background indexer and waits for it; the indexer's server refuses to start next to an editor's. Pid files in `.build/bsp/`, checked to be sourcekit processes. - `xcodebuild.sh [destination]` — Debug into Xcode's DerivedData with Xcode's settings (no `-derivedDataPath`: passing it changes build parameters and makes Xcode and VS Code rebuild after each other). `DDG_WORKTREE_CACHE=1`: `.build/xcodebuild` + shared CAS with `/^wt` `/^dd` maps. `.build/products` → products folder. xcbeautify + raw `path:line: error:` lines for failed tests. - `simulator.sh [--build-only|--no-build] [device]` — builds iOS Browser for a concrete simulator, installs, `simctl launch --wait-for-debugger`, writes `process attach --pid N` to `.build/simulator-attach.lldb`. - `index-in-background.py [--all] [--if-idle]` — headless indexing; `background-indexing.sh install` = launchd agent every 30 min. - `install-build-server.sh` — the central template builds the published fork at the pinned commit using its committed `Package.resolved`; the archival patch/lockfile snapshots remain for compatibility with earlier local installations. The original migration worktree's differing installer was preserved. ## Findings 1. **Works for app code out of the box**, no Package.swift. Swift extension 2.16.7 activates on `buildServer.json`. 2. **Tests: nothing.** BSP had no configuration → swift-build used `defaultConfigurationName = Release` → no testability → `@testable import` fails. Fix: `configuration` (default Debug; swift-build's own index build defaults to Debug too). 3. **Stray `.o`, failed links, duplicate outputs.** BSP configured all 977 targets in one build (DMG + App Store, iOS + macOS, each package library 3–4 times). Fix: `targets` filter → 86 roots, 288 in the graph, 0 failures. 4. **Indexing ~2 h.** swift-build computes `sourceKitOptions` per file (~0.8 s, single thread, whole build request each time). Swift args of a target differ only in the trailing `-index-unit-output-path`. Fix: per-target cache keyed by (target, purpose) + single flight. Fresh worktree full index ~5.5 min (M4 Max, low load). 5. **Preparation** (~250 serialized prepare builds) is now the critical path. 6. **Shared CAS between worktrees (xcodebuild):** needs `SWIFT/CLANG_ENABLE_PREFIX_MAPPING` + `*_OTHER_PREFIX_MAPPINGS` = `=/^dd =/^wt`. Clean build 179 s → 90–120 s, 0 misses in a new worktree. Xcode's own project prefix mapping (`/^src` per project) breaks lldb sources; `/^wt` + `sourceMap` works. Opt-in now, because it can't be shared with Xcode (unconfirmed how Xcode's debugger handles mapped paths). 7. **CAS doesn't help the language server** (prepare-for-index doesn't cache; cold vs warm the same). 8. **Index shared with Xcode works:** with Xcode's DerivedData layout (index store = `Index.noindex/DataStore`, `INDEX_REGULAR_BUILD_*` = `Build/…`), units written by an xcodebuild build count as up to date: after building `macOS Browser`, indexing skipped all app files (2,150 files left vs 3,972). BSP prepares in `Index.noindex/SourceKitBuild` so it never shares a build DB with an open Xcode. 9. **Xcode-compatible builds:** `xcodebuild` without `-derivedDataPath` and the wrapper don't rebuild after each other (no-op 26–31 s). With `-derivedDataPath` pointing to the same folder they do. 10. **iOS simulator:** generic destination fails (`fix_libswiftWebkit_simulator.sh` needs `TARGET_DEVICE_OS_VERSION`) → build for a concrete simulator. Debugging in one launch: `preLaunchTask` runs `simulator.sh` (launch `--wait-for-debugger`, writes the attach command), the config attaches with `attachCommands: command source .build/simulator-attach.lldb` (read at attach time, after the task). Earlier failures of `attachCommands`/`program` were caused by the wrong `sourceMap` (10a), not by lldb-dap. 10a. **Gray breakpoints in VS Code (macOS app):** the launch configs had `sourceMap` `/^wt → worktree` for all builds. lldb also applies it in reverse when setting breakpoints, so `/…/File.swift` became `/^wt/…/File.swift`, which regular (Xcode-mode) debug info doesn't contain → "no locations". Fix: no `sourceMap`; `xcodebuild.sh` writes `.build/debug.lldbinit` per build (source map only for `DDG_WORKTREE_CACHE=1`), launch configs `command source` it in `initCommands`. Confirmed fixed in VS Code 2026-10-15. 11. Index sharing between worktrees: rejected (absolute paths + mtime-based staleness). 12. `workspace/_pollIndex` isn't in release sourcekit-lsp → indexer waits for "Indexing" progress end + 60 s quiet; gives up after 5 min without progress. 13. swift-build service runs in its own process group → stop children first. 14. Unrelated: main's `SharedPackages/BrowserServicesKit/Package.resolved` is stale (C-S-S 17.20 vs `exact: 17.26.0`) — separate task spawned. ## Swift Build fork and cache findings — 2026-10-16 The existing fork is `/Users/admin/Developer/swift-build-custom`, origin `mallexxx/swift-build`, branch `cache-provisioning-inputs`, commit [0d23a4dd5eb536fcdbc10ef83bfdad57e6a7a022](https://github.com/mallexxx/swift-build/commit/0d23a4dd5eb536fcdbc10ef83bfdad57e6a7a022) from June 2025. Its patch serializes `ProvisioningTaskInputs` to `/ProvisioningTaskInputs/.provisioningTaskInputs.json` and reuses them during planning. **It caches signing/provisioning inputs, not package resolution or downloaded package artifacts.** That checkout has no ready `.build` service binary. The old cache has no configuration/profile/entitlement freshness key, and its Xcode 27.1 compatibility has not been validated. It was not enabled. After porting, building and checking compatibility, the existing wrapper can select a service for one invocation without a global shell export: ```sh XCBBUILDSERVICE_PATH=/absolute/products/path/SWBBuildServiceBundle \ scripts/vscode/xcodebuild.sh build 'macOS Browser' ``` The fork's `Plugins/run-xcodebuild/run-xcodebuild.swift` demonstrates the override. Keep service resources alongside the executable and match service/client versions. No provisioning-cache speedup was measured. **Current provisioning cost:** exact `GatherProvisioningInputs` section timestamps were extracted from five actual October 15 `macOS Browser` Debug build activity logs. All logged `XCODE_PRODUCT_BUILD_VERSION=27A9275`, matching the currently selected **Xcode 27.1 (27A9275)**. Durations were 0.621, 0.255, 1.111, 1.093 and 1.340 seconds: **median 1.09 s, range 0.25–1.34 s**. These are archived real-build measurements, not fresh full-build benchmarks or stdout approximations; they do not characterize cold signing/account setup or online provisioning updates. Evidence: `/Users/admin/DuckDuckGo/apple-browsers.git/vscode-local-setup/.build/.build-benchmark/provisioning-20261016/provisioning-summary.json`. **Package/artifact reuse plan (not automatically enabled):** keep per-worktree `SourcePackages` and Xcode-compatible DerivedData; share standard download/repository caches, and optionally seed a new worktree using independent APFS copies from a matching prepared cache before normal resolution. Avoid one shared mutable checkout directory across branches. A future fast path should fingerprint the toolchain, workspace/project files, manifests, lockfiles and package configuration, verify cache completeness, then use `-skipPackageUpdates -onlyUsePackageVersionsFromResolvedFile` only when valid. On changed/missing inputs, resolve normally before updating the stamp. These flags still evaluate the package graph in each new xcodebuild process. Existing wrapper arguments already permit an explicit warm-cache trial: ```sh scripts/vscode/xcodebuild.sh build 'macOS Browser' macOS \ -skipPackageUpdates -onlyUsePackageVersionsFromResolvedFile ``` The existing `DDG_WORKTREE_CACHE=1` mode is a separate compilation/CAS optimization; its earlier 179 s → 90–120 s measurements were not re-run during this rollout. ## Review (independent agent, 2026-10-14) Verdict: "close, not ready" → fixed: iOS platform reset by setup/indexer, unpinned `swift-tools-protocols` (now Package.resolved + `--force-resolved-versions`), purpose not in cache key, empty `include`, sources not generation-checked, test gaps (58 tests now), editor/indexer race (refuse + wait), stop() killing unchecked pids, indexer hanging 1 h, spaces in prefix maps, in-place install over running binaries, stale-install detection, test failures not in Problems, Xcode 26/27 wording, `re.escape`, `as_uri`, editor detection, xcbeautify missing warning, `files.watcherExclude`, lldb-dap pinned, risky `waitFor` attach removed, mismatched scheme/destination task. Not changed: `Debug` default (documented; matches swift-build's index default). ## Verified (Xcode 27.1) - Code intelligence: macOS app, macOS unit test (`@testable`, jump into app), package test, iOS app (ios mode) — in both layouts. - `Test macOS Unit Tests` task (33 tests pass), `Build macOS Browser`, iOS build for simulator, install, launch, lldb-dap attach + breakpoint; macOS launch + breakpoint (CLI lldb). - Background indexer: full run, skip when unchanged, editor takeover, no orphans. - Installer from scratch: patch applies, pinned build, stamp. Not verified: VS Code/Cursor UI itself; Xcode IDE reading our index / no-rebuild after VS Code builds (xcodebuild was used as Xcode's stand-in). ## Open items - [ ] Walk through VS Code and Cursor UI (tasks, Problems, launch/attach, workspace trust prompt). - [ ] Check Xcode IDE: open a worktree indexed/built from VS Code — no reindex, no rebuild. - [x] Publish tested fixes to [mallexxx/sourcekit-xcode-bsp](https://github.com/mallexxx/sourcekit-xcode-bsp), pinned at `afd0e3dd026b3ee3619e2271809debee25fec96f`. - [x] Deploy shared local hook and backfill all 97 registered worktrees (2026-10-16). - [ ] Establish a DuckDuckGo-owned fork/team rollout if approved. - [ ] Offer the build-server fixes upstream (no upstream PR opened). - [ ] Upstream issue: duplicate outputs/stray `.o` when all targets are configured; lldb-dap attach with `program`/`attachCommands` to simulator processes. - [ ] PR (needs Asana task link).