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

7.1 KiB

Tech Design: VS Code and Cursor support through an Xcode 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/1219079700626622?focus=true

Background & Requirements

  • Some of us (would prefer to) spend much of the day outside Xcode: in VS Code, reviewing changes, working with AI agents, or across several worktrees at once. There, the Swift language server has no build information for our Xcode projects. So there's no direct build&run support, no jump to definition, no completion and no errors as you type, and no debugger.
  • The previous tech design tried to solve this by making the macOS browser code a Swift package. The team decided not to do it at this stage. The concerns were:
    • a more custom, more fragile setup;
    • resources that can fail silently at runtime;
    • no good way to have per-configuration dependencies;
    • macOS and iOS drifting apart;
    • an unclear number of users and an unclear owner.
  • The Swift language server (sourcekit-lsp) can get build information from a build server instead of a Swift package. sourcekit-xcode-bsp is an open-source build server that reads .xcodeproj / .xcworkspace with swift-build, the build engine Apple open-sourced from Xcode.
  • Requirements:
    • No change to the Xcode projects, schemes, CI or release, and nothing changes for people who only use Xcode.
    • Opt-in and easy to remove.
    • macOS and iOS, including tests and packages.
    • People who use both Xcode and VS Code don't build or index everything twice.

Problem Statement

Code intelligence and debugging in VS Code and Cursor need build information that today only Xcode has. We want to provide it from the Xcode projects as they are, without restructuring the codebase around it.

Add an opt-in VS Code setup to the repository: a workspace file and a few scripts in scripts/vscode/. Nothing else in the repository changes.

What a developer gets

  • Completion, jump to definition, references and live errors for app, test and package code, on macOS or iOS. One platform is chosen per worktree and can be switched.
  • Tasks to build any scheme and run unit tests; errors and failed tests show in the Problems panel.
  • Debugging with breakpoints:
    • the macOS app, launched from the editor;
    • the iOS app on a simulator, by running the app and then attaching.
  • Shared with Xcode. Builds and the index live in the worktree's usual Xcode DerivedData folder. Whatever Xcode built or indexed is ready in VS Code, and the other way round.
  • Optional background indexing of worktrees while no editor is open.

How it works

  • The workspace generates the build server's config on folder open. The config chooses the platform and the targets to index, the same way an Xcode scheme does.
  • Builds and tests go through xcodebuild with the same settings Xcode uses.
  • The build server is installed locally by a script, from a pinned version.

The build server needed fixes. On our workspace it gave no results in test files, broke on the DMG/App Store variants and took hours to index. Four small changes fix this (see Appendix), and a worktree now indexes in about 5 minutes. They're general improvements, so the plan is to contribute them upstream. Until they're merged, the repository carries them as a patch that the install script applies.

Steps

  1. Land the setup in the repository, plus a short README.
  2. Open PRs for the build server changes in its repository.
  3. A few early adopters on macOS and iOS try it for a couple of weeks; then we decide whether to announce it more widely.

Pros

  • No project changes. The Xcode projects stay the only source of truth. Removing the setup means deleting scripts/vscode/ and the workspace file.
  • Same build as Xcode. swift-build reads the projects, so per-configuration settings and dependencies, resources, macros and plugins behave like in Xcode.
  • Nothing to maintain in the app code. No lint rules or checks to keep the setup working.
  • Both platforms, so it doesn't add a gap between macOS and iOS.
  • No cost for Xcode users, and those who use both tools share one build and one index.

Cons

  • A young third-party tool (version 0.1.0, "early-stage" by its authors), and our patch on top of it until upstream accepts the changes.
  • Some rough edges:
    • one platform per worktree at a time;
    • iOS debugging is two steps (run, then attach);
    • the first index of a new worktree takes ~5 minutes.
  • Someone has to own it. I will; the setup is small (a workspace file and ~6 scripts), and it's documented.

Risks

  • Xcode updates can break the build server, since it bundles its own swift-build. Mitigation: a pinned version and a check after each Xcode update. The worst case is that the setup stops working for the people who opted in; Xcode and CI aren't affected.
  • Upstream doesn't accept the changes or the project is abandoned. Mitigation: keep a fork under the DuckDuckGo organization (the changes are ~550 lines plus tests), or drop the setup.
  • Xcode and the editor share the index. Both write to Xcode's index store; deleting the DerivedData folder resets it, as today.

Testing

  • Code intelligence: macOS and iOS app files, app unit tests (including jumping into app code), package tests.
  • Tasks:
    • building macOS and iOS schemes;
    • running unit tests;
    • builds from Xcode and VS Code don't rebuild each other's work.
  • Debugging: breakpoints in the macOS app and in the iOS app on a simulator.
  • The build server changes have unit tests.
  • Before announcing: two people try it on fresh machines in VS Code and Cursor, and we check that Xcode picks up a worktree built and indexed from VS Code without rebuilding.

Additional Considerations

  • Privacy / Security: No change to the apps. The install script builds the build server from its public repository at a pinned version, and everything runs locally.
  • Site Breakage / Experimentation / Localization: Not applicable.
  • Operational: No CI or release change.

Appendix: changes to the build server

  1. Debug configuration by default. Without one, swift-build used the projects' default configuration, Release, which can't import app modules into tests, so test files had no results.
  2. Choosing targets. It configured all ~980 workspace targets in one build, including DMG and App Store variants of the same products, which write to the same paths. It now takes include/exclude patterns, so we configure what a scheme would.
  3. Caching compiler arguments per target. It computed them separately for every file, ~1 s each, one at a time, which made indexing take hours. With the cache, a fresh worktree indexes in ~5 minutes on an M4 Max.
  4. Xcode's DerivedData layout, so the index and builds are shared with Xcode.

Research notes with measurements: projects/vscode-migration/xcode-build-server.