8.5 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 has raised some concerns against doing 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/.xcworkspacewith 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 autocompletion, jump-to-definition 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.
Recommended Approach
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
xcodebuildwith the same settings Xcode uses. - The build server is installed locally by a script that builds it from source at a pinned commit of our fork (see below).
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 we'll offer them upstream. We don't depend on upstream accepting them: we fork the build server into a DuckDuckGo repository, keep our changes there and pull upstream changes manually after reviewing them.
Steps
- Fork sourcekit-xcode-bsp into a DuckDuckGo repository with our changes.
- Land the setup in the repository, plus a short README.
- Offer the changes upstream as PRs.
- 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) that we fork and maintain ourselves.
- 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.
- Maintenance as Xcode, swift-build and the build server move. Xcode updates or upstream changes can need updates to the fork or the setup. Mitigation: the people who use the setup do the updates, mostly with AI agents (the setup is small and has a README and tests). Xcode-only users and CI never depend on it.
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.
- Malicious code in the build server or its dependencies (supply chain). It runs on developer machines with access to the source code and the network. Mitigations:
- we build it from source at a pinned commit of our fork; nothing is downloaded prebuilt or updated automatically;
- its own code is small (~1,700 lines in 13 Swift files upstream, plus ~550 lines of our changes), so each upstream update can be reviewed as a diff before it goes into the fork;
- its dependencies come from Apple/swiftlang (swift-build, swift-tools-protocols, swift-argument-parser) and are pinned too.
- Upstream doesn't accept the changes or the project is abandoned. Mitigation: the fork already carries our changes, so nothing breaks; we keep it, 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 and no user data involved; the setup only runs locally on developer machines. It does add third-party code to those machines, built from our fork at a pinned commit (see Risks). Question for reviewers: does a developer tool like this need a privacy/security triage? It's a small open-source package with Apple-only dependencies, so a light review of the fork may be enough.
- Site Breakage / Experimentation / Localization: Not applicable.
- Operational: No CI or release change.
Appendix: changes to the build server
- 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.
- 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.
- 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.
- Xcode's DerivedData layout, so the index and builds are shared with Xcode.
Research notes with measurements: projects/vscode-migration/xcode-build-server.