diff --git a/work/projects/vscode-migration/tech-design-xcode-build-server.md b/work/projects/vscode-migration/tech-design-xcode-build-server.md new file mode 100644 index 00000000..6dc5a4e3 --- /dev/null +++ b/work/projects/vscode-migration/tech-design-xcode-build-server.md @@ -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 `/`, 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. diff --git a/work/projects/vscode-migration/vscode-migration.md b/work/projects/vscode-migration/vscode-migration.md index c93c3e4f..ed500da7 100755 --- a/work/projects/vscode-migration/vscode-migration.md +++ b/work/projects/vscode-migration/vscode-migration.md @@ -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 diff --git a/work/projects/vscode-migration/xcode-build-server.md b/work/projects/vscode-migration/xcode-build-server.md new file mode 100644 index 00000000..71dc8215 --- /dev/null +++ b/work/projects/vscode-migration/xcode-build-server.md @@ -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 [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. +- [ ] Clean up experiment worktrees and caches. +- [ ] PR (needs Asana task link).