Files
Kabir Oberai 6efd77fc0e sdk: self-updating darwin tools (#110)
## What does this PR do?

Allows xtool to auto-update the Darwin SDK format when there's a new
Darwin toolset, or when we change the SDK format. Downstream this also
allows us to auto-update when there's changes to OpenAppleMacros (#101).

One callout: the disk footprint of `darwin.artifactbundle` is now ~7 GB
instead of ~3 GB.

Reason: `xtool sdk install /path/to/Xcode.[app|xip]` now performs a
"normal" install by default, which preserves the entirety of
`Xcode.app/Contents/Developer` — even the bits we don't currently use,
like `AppleTVOS.platform`. This makes it so if we start using more
components in the future, the user doesn't need to download Xcode again
and rebuild the SDK themselves.

You can still build an old-style SDK (containing just the components we
need) using `xtool sdk install --slim` or the dedicated `xtool sdk
build`.

## How was it tested?

Tested all of the install modes. Confirmed that auto-updating works.
Added integration tests in #252.

## 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 SDK flavor reporting for slim, normal, and legacy installations.
- Added `sdk update` support to rebuild and install SDKs from an
installed Xcode bundle.
  - Added a `--slim` option for SDK installation.
  - SDK status now displays installation path, flavor, and version.
- SDK setup and build operations now ensure a suitable SDK is available
automatically.

- **Bug Fixes**
- Improved temporary SDK bundle handling, validation, replacement, and
cross-filesystem copying.
  - Copy failures now provide detailed diagnostic information.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-18 23:26:10 -04:00

124 lines
4.9 KiB
Swift

import Foundation
import XUtils
import Subprocess
import Superutils
public struct DarwinSDK {
package struct TemporaryBundle: ~Copyable {
package let url: URL
fileprivate init(url: URL) {
self.url = url
}
package consuming func install() async throws {
guard DarwinSDK(bundle: url) != nil else {
throw StringError("Invalid Darwin SDK at '\(url.path)'")
}
try await DarwinSDK.addHostClangResourceDir(to: url)
let destination = url.deletingLastPathComponent()
.appendingPathComponent("darwin.artifactbundle", isDirectory: true)
try FileManager.default.moveItem(at: url, to: destination)
}
deinit {
try? FileManager.default.removeItem(at: url)
}
}
public enum Flavor {
// can't be updated in place
case slim
// can be updated in place, includes a whole copy of Xcode.app
case normal
// from before the slim/normal split existed (version "develop")
case legacy
}
public let bundle: URL
public let version: String
public let flavor: Flavor
static func swiftPMDirectory(
environment: [String: String] = ProcessInfo.processInfo.environment,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser
) throws -> URL {
if let configurationDirectory = environment["XDG_CONFIG_HOME"] {
guard (configurationDirectory as NSString).isAbsolutePath else {
throw StringError("XDG_CONFIG_HOME must be an absolute path: '\(configurationDirectory)'")
}
return URL(fileURLWithPath: configurationDirectory, isDirectory: true)
.appendingPathComponent("swiftpm", isDirectory: true)
} else {
return homeDirectory.appendingPathComponent(".swiftpm", isDirectory: true)
}
}
private static func swiftSDKsDirectory(
environment: [String: String] = ProcessInfo.processInfo.environment,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser
) throws -> URL {
try swiftPMDirectory(environment: environment, homeDirectory: homeDirectory)
.appendingPathComponent("swift-sdks", isDirectory: true)
}
public init?(bundle: URL) {
self.bundle = bundle
if let version = try? Data(contentsOf: bundle.appendingPathComponent("darwin-sdk-version.txt")) {
self.version = String(decoding: version, as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
} else if ["darwin.xtoolsdk", "darwin.artifactbundle", "darwin.artifactbundle.tmp"].contains(bundle.lastPathComponent) {
self.version = "develop"
} else {
return nil
}
if version == "develop" {
self.flavor = .legacy
} else if bundle.appendingPathComponent("Xcode.app").dirExists {
self.flavor = .normal
} else {
self.flavor = .slim
}
}
package static func prepareTemporaryBundle(
environment: [String: String] = ProcessInfo.processInfo.environment,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser
) throws -> TemporaryBundle {
let sdksDirectory = try swiftSDKsDirectory(environment: environment, homeDirectory: homeDirectory)
try FileManager.default.createDirectory(at: sdksDirectory, withIntermediateDirectories: true)
let bundle = sdksDirectory.appendingPathComponent("darwin.artifactbundle.tmp", isDirectory: true)
if FileManager.default.fileExists(atPath: bundle.path) {
try FileManager.default.removeItem(at: bundle)
}
return TemporaryBundle(url: bundle)
}
private static func addHostClangResourceDir(to sdk: URL) async throws {
let clangURL = try await ToolRegistry.locate("clang")
let process = try await Subprocess.run(
.path(FilePath(clangURL.path)),
arguments: ["-print-resource-dir"],
output: .string(limit: .max)
).checkSuccess()
let output = process.standardOutput ?? ""
let hostClangResources = URL(filePath: output.trimmingCharacters(in: .whitespacesAndNewlines))
let hostInclude = hostClangResources.appending(path: "include")
let sdkInclude = sdk.appending(path: "Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/clang/include")
try await FileManager.default.copyItem(at: hostInclude, to: sdkInclude, preserveOwner: false)
}
public static func current() throws -> DarwinSDK? {
let bundle = try swiftSDKsDirectory().appendingPathComponent("darwin.artifactbundle", isDirectory: true)
guard bundle.dirExists else { return nil }
return DarwinSDK(bundle: bundle)
}
public func remove() throws {
try FileManager.default.removeItem(at: bundle)
}
}