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

91 lines
3.0 KiB
Swift

import Foundation
import PackLib
enum LSPConfig {
static let bsp: (String, String) = (
".bsp/xtool.json",
"""
{
"name": "xtool",
"version": "1.0",
"bspVersion": "2.2.0",
"languages": ["c", "cpp", "objective-c", "objective-cpp", "swift"],
"argv": ["/usr/bin/env", "xtool", "dev", "build-server"]
}
"""
)
static let legacyLSP: (String, String) = (
".sourcekit-lsp/config.json",
"""
{
"swiftPM": {
"swiftSDK": "arm64-apple-ios"
}
}
"""
)
nonisolated(nonsending) static func tryEnsure(
using schema: PackSchema,
quiet: Bool = false,
currentDirectory directory: URL = URL(filePath: FileManager.default.currentDirectoryPath)
) async {
do {
try await ensure(using: schema, quiet: quiet, currentDirectory: directory)
} catch {
print("warning: failed to validate LSP configuration: \(error)")
}
}
nonisolated(nonsending) static func ensure(
using schema: PackSchema,
quiet: Bool = false,
currentDirectory directory: URL = URL(filePath: FileManager.default.currentDirectoryPath)
) async throws {
if schema.skipLSP == true {
return
}
if try await SwiftVersion.current.supportsSwiftBuild {
try await ensure(primary: bsp, secondary: legacyLSP, quiet: quiet, currentDirectory: directory)
} else {
try await ensure(primary: legacyLSP, secondary: bsp, quiet: quiet, currentDirectory: directory)
}
}
// creates the primary LSP-support file if it doesn't exist, and cleans up old files
private nonisolated(nonsending) static func ensure(
primary: (String, String),
secondary: (String, String),
quiet: Bool,
currentDirectory directory: URL
) async throws {
let primaryURL = directory.appendingPathComponent(primary.0)
if FileManager.default.fileExists(atPath: primaryURL.path) {
return
}
try? FileManager.default.createDirectory(
at: primaryURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try Data((primary.1 + "\n").utf8).write(to: primaryURL)
let secondaryURL = directory.appendingPathComponent(secondary.0)
if let secondaryData = try? await Data(reading: secondaryURL),
let secondaryString = String(data: secondaryData, encoding: .utf8),
secondaryString.trimmingCharacters(in: .whitespacesAndNewlines) == secondary.1 {
try? FileManager.default.removeItem(at: secondaryURL)
}
if !quiet {
print("""
[LSP] Created \(primary.0) for enhanced IDE support. You may
need to restart your editor for the change to take effect.
You can disable this with `skipLSP: true` in xtool.yml.
""")
}
}
}