Files
Kabir Oberai 0108899e04 sdk: SwiftBuild support (#229)
## What does this PR do?

Adds support for SwiftBuild on Swift 6.4.

We now point SwiftBuild to iPhoneOS.platform and rely on the real
[SWBApplePlatform](https://github.com/swiftlang/swift-build/tree/b30ee3c00abf34857084c26bb873c3da06348134/Sources/SWBApplePlatform).
We also do a little futzing to convince SWB to use our Darwin-compatible
toolset (lld, librarian, resource dirs). We explicitly _don't_ pass in a
Swift SDK because that would downgrade SWB to using one of the generic
platform plugins instead of ApplePlatform.

This also gets us a lot closer to parity with Xcode's behavior when
building for Apple platforms; we get the same build flags and
everything. We can probably achieve identical bundling too, if we take
control of the PIF.

One other change: since we need to pass custom env vars and such, we
can't rely on sourcekit-lsp's built-in SwiftPM integration anymore. We
thus create a custom BSP server that trampolines to SwiftPM's build
server with the right flags set. This effectively makes SwiftPM an
implementation detail, so we can start messing with the PIF.

## How was it tested?

- [x] Build with Swift 6.3 on macOS
- [x] Build with Swift 6.4 on macOS
- [x] Build with Swift 6.3 on Linux
- [x] Build with Swift 6.4 on Linux
- [x] BSP/LSP file creation works
- [x] LSP support works in VSCode

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [ ] **0** - No AI was used to write code
- [x] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added a `dev build-server` command for Build Server Protocol
integration.
- Projects automatically select the appropriate Swift build system when
supported.
- Packaging supports output layouts for Swift Package Manager and Swift
Build.
  - Added options to skip language-server configuration when needed.

- **Documentation**
- Updated project templates and tutorials to use `.bsp/xtool.json` for
IDE features such as syntax highlighting and documentation lookup.

- **Improvements**
- Enhanced SDK generation and compatibility across supported toolchains
and platforms.
- Improved automatic language-server configuration while preserving
existing configuration files.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-20 02:47:12 -04:00

288 lines
8.9 KiB
Swift

import Foundation
import XKit
import Version
import ArgumentParser
import Dependencies
import PackLib
import XUtils
import Subprocess
struct SDKCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "sdk",
abstract: "Manage the Darwin Swift SDK",
subcommands: [
DevSDKInstallCommand.self,
DevSDKUpdateCommand.self,
DevSDKRemoveCommand.self,
DevSDKBuildCommand.self,
DevSDKStatusCommand.self,
],
defaultSubcommand: DevSDKInstallCommand.self
)
}
struct DevSDKBuildCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "build",
abstract: "Build the Darwin SDK from Xcode.xip"
)
@Argument(
help: "Path to Xcode.xip or Xcode.app",
completion: .file(extensions: ["xip", "app"])
)
var path: String
@Argument(
help: "Output directory"
)
var outputDir: String
@Option(
help: ArgumentHelp(
"The architecture of the Linux host the SDK is being built for.",
discussion: "Defaults to 'auto', which attempts to match the current host architecture."
)
) var arch: ArchSelection = .auto
func run() async throws {
let builderArch = try arch.sdkBuilderArch
let input = try SDKBuilder.Input(path: path)
let output = URL(fileURLWithPath: outputDir, isDirectory: true).appending(path: "darwin.xtoolsdk")
let builder = SDKBuilder(input: input, output: output, arch: builderArch, mode: .buildSlim)
try await builder.buildSDK()
print("Built SDK at \(output.path). You can install it with `xtool sdk install`.")
}
}
enum ArchSelection: String, ExpressibleByArgument {
case auto
case x86_64
case arm64
var sdkBuilderArch: SDKBuilder.Arch {
get throws {
switch self {
case .auto:
#if arch(arm64)
.aarch64
#elseif arch(x86_64)
.x86_64
#else
throw Console.Error("Could not auto-detect target architecture. Please specify one with '--arch'.")
#endif
case .arm64: .aarch64
case .x86_64: .x86_64
}
}
}
}
struct DevSDKInstallCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "install",
abstract: "Install the Darwin Swift SDK"
)
@Argument(
help: "Path to Xcode.xip, Xcode.app, or darwin.xtoolsdk",
completion: .file(extensions: ["xip", "app", "xtoolsdk"])
)
var path: String
@Flag(
help: "Install a slim SDK (uses less disk space). Slim SDKs cannot be updated in place."
)
var slim = false
func run() async throws {
try await InstallSDKOperation(path: path, slim: slim).run()
}
}
struct DevSDKUpdateCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "update",
abstract: "Update the installed Darwin Swift SDK"
)
func run() async throws {
try await UpdateSDKOperation().run()
}
}
struct DevSDKRemoveCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "remove",
abstract: "Remove the Darwin Swift SDK"
)
func run() async throws {
guard let sdk = try DarwinSDK.current() else {
throw Console.Error("Cannot remove SDK: no Darwin SDK installed")
}
try sdk.remove()
print("Uninstalled SDK")
}
}
struct DevSDKStatusCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "status",
abstract: "Get the status of the Darwin Swift SDK"
)
func run() async throws {
if let sdk = try DarwinSDK.current() {
print("Darwin SDK is installed")
print(" Path: \(sdk.bundle.path)")
print(" Flavor: \(sdk.flavor)")
print(" Version: \(sdk.version)")
} else {
print("Not installed")
}
}
}
extension DarwinSDK {
func isUpToDate() -> Bool {
version == SDKBuilder.currentSDKVersion
}
}
struct EnsureSDKOperation {
let quiet: Bool
func run() async throws {
#if os(macOS)
if !quiet {
print("Skipping Darwin SDK setup since we're on macOS.")
}
#else
let sdk = try DarwinSDK.current()
switch sdk.map({ ($0.isUpToDate(), $0.flavor) }) {
case (true, _)?: // swiftlint:disable:this optional_enum_case_matching
if !quiet {
print("Darwin SDK is up to date.")
}
case (false, .slim)?: // swiftlint:disable:this optional_enum_case_matching
throw Console.Error("""
Darwin SDK is out of date, and was installed in 'slim' mode.
Slim SDKs take less disk space, but can't be auto-updated.
Please install a new SDK with
xtool sdk install [--slim]
""")
case (false, .normal)?: // swiftlint:disable:this optional_enum_case_matching
print("Darwin SDK is out of date. Rebuilding...")
try await UpdateSDKOperation().run()
case (false, .legacy)?: // swiftlint:disable:this optional_enum_case_matching
print("""
Darwin SDK is incompatible: built with an older version of xtool.
Requesting re-install. This is a one-time rebuild; after this,
xtool will be able to resolve incompatibilities automatically.
""")
try await generateSDK()
case nil:
print("Now generating the Darwin SDK.\n")
try await generateSDK()
}
func generateSDK() async throws {
let path = try await Console.prompt("""
Please download Xcode from http://developer.apple.com/download/all/?q=Xcode
and enter the path to the downloaded Xcode.xip.
Path to Xcode.xip: \("" /* pacify swiftlint trailing_whitespace */)
""")
let expanded = (path as NSString).expandingTildeInPath
try await InstallSDKOperation(path: expanded).run()
}
#endif
}
}
struct InstallSDKOperation {
let path: String
let slim: Bool
init(path: String, slim: Bool = false) {
self.path = path
self.slim = slim
}
func run() async throws {
#if os(macOS)
print("Skipping SDK install; the iOS SDK ships with Xcode on macOS")
#else
let temporaryBundle = try DarwinSDK.prepareTemporaryBundle()
if path.hasSuffix(".xtoolsdk") {
print("Installing prebuilt SDK...")
try await FileManager.default.copyItem(at: URL(filePath: path), to: temporaryBundle.url, preserveOwner: false)
} else {
// validate input before removing existing SDK
let input = try SDKBuilder.Input(path: path)
let arch = try ArchSelection.auto.sdkBuilderArch
let mode: SDKBuilder.Mode = slim ? .buildSlim : .buildNormal
let builder = SDKBuilder(input: input, output: temporaryBundle.url, arch: arch, mode: mode)
try await builder.buildSDK()
}
if let sdk = try DarwinSDK.current() {
print("Removing existing SDK...")
try sdk.remove()
}
try await temporaryBundle.install()
#endif
}
}
struct UpdateSDKOperation {
func run() async throws {
#if os(macOS)
print("Skipping SDK install; the iOS SDK ships with Xcode on macOS")
#else
guard let existing = try DarwinSDK.current() else {
throw Console.Error("Could not locate existing SDK; cannot perform update.")
}
let xcode = existing.bundle.appendingPathComponent("Xcode.app")
guard xcode.dirExists else {
// This includes prebuilt .xtoolsdk installs and installs created with --slim.
throw Console.Error("""
The installed SDK was built in 'slim' mode and cannot be updated in place. \
Please install a new copy with `xtool sdk install`.
""")
}
let input = try SDKBuilder.Input(path: xcode.path)
let arch = try ArchSelection.auto.sdkBuilderArch
let temporaryBundle = try DarwinSDK.prepareTemporaryBundle()
let builder = SDKBuilder(input: input, output: temporaryBundle.url, arch: arch, mode: .update)
try await builder.buildSDK()
guard DarwinSDK(bundle: temporaryBundle.url) != nil else {
throw Console.Error("Invalid Darwin SDK at '\(temporaryBundle.url.path)'")
}
try FileManager.default.moveItem(
at: xcode,
to: temporaryBundle.url.appendingPathComponent("Xcode.app")
)
try existing.remove()
try await temporaryBundle.install()
print("Updated SDK")
#endif
}
}