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

180 lines
5.5 KiB
Swift

import ArgumentParser
import Foundation
import PackLib
struct NewCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "new",
abstract: "Create a new xtool SwiftPM project"
)
@Argument var name: String?
@Flag(
help: ArgumentHelp(
"Skip setup steps. (default: false)",
discussion: """
By default, this command first invokes `xtool setup` to complete \
any missing setup steps, like authenticating and installing the SDK. Use \
this flag to always skip setup.
"""
)
) var skipSetup = false
// swiftlint:disable:next function_body_length
func run() async throws {
if !skipSetup {
// perform any remaining setup steps first
try await SetupOperation(quiet: true).run()
}
let name = try await Console.promptRequired("Package name: ", existing: self.name)
var allowedFirstCharacters: CharacterSet = ["_"]
allowedFirstCharacters.insert(charactersIn: "a"..."z")
allowedFirstCharacters.insert(charactersIn: "A"..."Z")
var allowedOtherCharacters = allowedFirstCharacters
allowedOtherCharacters.insert(charactersIn: "0"..."9")
allowedOtherCharacters.insert("-")
// promptRequired validates !isEmpty
let firstScalar = name.unicodeScalars.first!
guard allowedFirstCharacters.contains(firstScalar) else {
throw Console.Error("""
Package name '\(name)' is invalid. \
The package name must start with one of [a-z, A-Z, _]. Found '\(firstScalar)'.
""")
}
if let firstInvalid = name.rangeOfCharacter(from: allowedOtherCharacters.inverted) {
let invalidValue = name[firstInvalid]
throw Console.Error("""
Package name '\(name)' is invalid. \
The package name may only contain [a-z, A-Z, 0-9, _, -]. Found '\(invalidValue)'.
""")
}
let baseURL = URL(fileURLWithPath: name)
guard !baseURL.exists else {
throw Console.Error("Cannot create \(name): a file already exists at that path.")
}
print("Creating package: \(name)")
let moduleName = name.replacingOccurrences(of: "-", with: "_")
let files: [(String, String)] = [
(
"Package.swift",
"""
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "\(name)",
platforms: [
.iOS(.v17),
.macOS(.v14),
],
products: [
// An xtool project should contain exactly one library product,
// representing the main app.
.library(
name: "\(moduleName)",
targets: ["\(moduleName)"]
),
],
targets: [
.target(
name: "\(moduleName)"
),
]
)
"""
),
(
"xtool.yml",
"""
version: 1
bundleID: com.example.\(name)
"""
),
(
".gitignore",
"""
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/xtool
"""
),
(
"Sources/\(moduleName)/\(moduleName)App.swift",
"""
import SwiftUI
@main
struct \(moduleName)App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
"""
),
(
"Sources/\(moduleName)/ContentView.swift",
"""
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
"""
),
]
for (path, contents) in files {
let url = baseURL.appendingPathComponent(path)
try? FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
print("Creating \(path)")
try "\(contents)\n".write(to: url, atomically: true, encoding: .utf8)
}
await LSPConfig.tryEnsure(
using: .default,
quiet: true,
currentDirectory: baseURL,
)
print("\nFinished generating project \(name). Next steps:")
print("- Enter the directory with `cd \(name)`")
print("- Build and run with `xtool dev`")
}
}