Skip to content

Commit 3a05519

Browse files
Add support for targeting FreeBSD
The Swift SDK Generator project should be able to generate SDKs that target FreeBSD systems. FreeBSD is much simpler than Linux. We only require the user to provide their desired architecture and FreeBSD version. This script will then download the FreeBSD base system content and create a sysroot suitable for use with Swift Package Manager. Because there are not currently any Swift toolchains for FreeBSD available on swift.org, we require the user to provide a path to a pre-built toolchain if they want the SDK to support building Swift code. In the future, we can download the Swift toolchain like we do for Linux in lieu of requiring the user to provide it.
1 parent 349caaf commit 3a05519

File tree

5 files changed

+304
-3
lines changed

5 files changed

+304
-3
lines changed

Sources/GeneratorCLI/GeneratorCLI.swift

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ struct GeneratorCLI: AsyncParsableCommand {
2323

2424
static let configuration = CommandConfiguration(
2525
commandName: "swift-sdk-generator",
26-
subcommands: [MakeLinuxSDK.self, MakeWasmSDK.self],
26+
subcommands: [MakeLinuxSDK.self, MakeFreeBSDSDK.self, MakeWasmSDK.self],
2727
defaultSubcommand: MakeLinuxSDK.self
2828
)
2929

@@ -304,6 +304,73 @@ extension GeneratorCLI {
304304
}
305305
}
306306

307+
struct MakeFreeBSDSDK: AsyncParsableCommand {
308+
static let configuration = CommandConfiguration(
309+
commandName: "make-freebsd-sdk",
310+
abstract: "Generate a Swift SDK bundle for FreeBSD.",
311+
discussion: """
312+
The default `--target` triple is FreeBSD for x86_64
313+
"""
314+
)
315+
316+
@OptionGroup
317+
var generatorOptions: GeneratorOptions
318+
319+
@Option(help: "Swift toolchain for FreeBSD from which to copy the Swift libraries. This must match the architecture of the target triple.")
320+
var fromSwiftToolchain: String? = nil
321+
322+
@Option(
323+
help: """
324+
Version of FreeBSD to use as a target platform. Example: 14.3
325+
"""
326+
)
327+
var freebsdVersion: String?
328+
329+
func deriveTargetTriple() -> Triple {
330+
if let target = generatorOptions.target, target.os == .freeBSD {
331+
return target
332+
}
333+
if let arch = generatorOptions.targetArch {
334+
let target = Triple(arch: arch, vendor: nil, os: .freeBSD)
335+
appLogger.warning(
336+
"Using `--target-arch \(arch)` defaults to `\(target.triple)`. Use `--target` if you want to pass the full target triple."
337+
)
338+
return target
339+
} else {
340+
// If no target is given, assume x86_64.
341+
return Triple(arch: .x86_64, vendor: nil, os: .freeBSD)
342+
}
343+
}
344+
345+
func run() async throws {
346+
let freebsdDefaultVersion = "14.3"
347+
let freebsdVersionString =
348+
self.freebsdVersion ?? freebsdDefaultVersion
349+
let freebsdVersion = try FreeBSD(freebsdVersionString)
350+
let hostTriple = try self.generatorOptions.deriveHostTriple()
351+
let targetTriple = self.deriveTargetTriple()
352+
353+
let sourceSwiftToolchain: FilePath?
354+
if let fromSwiftToolchain {
355+
sourceSwiftToolchain = .init(fromSwiftToolchain)
356+
} else {
357+
sourceSwiftToolchain = nil
358+
}
359+
360+
let recipe = FreeBSDRecipe(
361+
freeBSDVersion: freebsdVersion,
362+
mainTargetTriple: targetTriple,
363+
sourceSwiftToolchain: sourceSwiftToolchain,
364+
logger: loggerWithLevel(from: self.generatorOptions),
365+
)
366+
try await GeneratorCLI.run(
367+
recipe: recipe,
368+
targetTriple: targetTriple,
369+
options: self.generatorOptions
370+
)
371+
}
372+
}
373+
307374
struct MakeWasmSDK: AsyncParsableCommand {
308375
static let configuration = CommandConfiguration(
309376
commandName: "make-wasm-sdk",

Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ public actor SwiftSDKGenerator {
149149
self.fileManager.fileExists(atPath: path.string)
150150
}
151151

152+
func doesDirectoryExist(at path: FilePath) -> Bool {
153+
var isDirectory: ObjCBool = false
154+
let itemExists = self.fileManager.fileExists(atPath: path.string, isDirectory: &isDirectory)
155+
return itemExists && isDirectory.boolValue
156+
}
157+
152158
func removeFile(at path: FilePath) throws {
153159
try self.fileManager.removeItem(atPath: path.string)
154160
}
@@ -245,16 +251,24 @@ public actor SwiftSDKGenerator {
245251
func untar(
246252
file: FilePath,
247253
into directoryPath: FilePath,
248-
stripComponents: Int? = nil
254+
stripComponents: Int? = nil,
255+
paths: [String]? = nil,
249256
) async throws {
250257
let stripComponentsOption: String
251258
if let stripComponents {
252259
stripComponentsOption = "--strip-components=\(stripComponents)"
253260
} else {
254261
stripComponentsOption = ""
255262
}
263+
264+
var command = #"tar -C "\#(directoryPath)" \#(stripComponentsOption) -xf "\#(file)""#
265+
if let paths {
266+
for path in paths {
267+
command += #" "\#(path)""#
268+
}
269+
}
256270
try await Shell.run(
257-
#"tar -C "\#(directoryPath)" \#(stripComponentsOption) -xf "\#(file)""#,
271+
command,
258272
shouldLogCommands: self.isVerbose
259273
)
260274
}
@@ -298,6 +312,8 @@ public actor SwiftSDKGenerator {
298312
} else {
299313
try await self.gunzip(file: file, into: directoryPath)
300314
}
315+
case "txz":
316+
try await self.untar(file: file, into: directoryPath)
301317
case "deb":
302318
try await self.unpack(debFile: file, into: directoryPath)
303319
case "pkg":
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
public struct FreeBSD: Sendable {
16+
public let version: String
17+
18+
public let majorVersion: Int
19+
public let minorVersion: Int
20+
21+
public init(_ versionString: String) throws {
22+
guard !versionString.isEmpty else {
23+
throw GeneratorError.invalidVersionString(
24+
string: versionString,
25+
reason: "The version string cannot be empty."
26+
)
27+
}
28+
29+
let versionComponents = versionString.split(separator: ".")
30+
guard let majorVersion = Int(versionComponents[0]) else {
31+
throw GeneratorError.unknownFreebsdVersion(version: versionString)
32+
}
33+
34+
self.version = versionString
35+
self.majorVersion = majorVersion
36+
if versionComponents.count > 1, let minorVersion = Int(versionComponents[1]) {
37+
self.minorVersion = minorVersion
38+
} else {
39+
minorVersion = 0
40+
}
41+
}
42+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
import Helpers
15+
import Logging
16+
17+
import struct SystemPackage.FilePath
18+
19+
package struct FreeBSDRecipe: SwiftSDKRecipe {
20+
package func applyPlatformOptions(metadata: inout SwiftSDKMetadataV4, paths: PathsConfiguration, targetTriple: Triple, isForEmbeddedSwift: Bool) {
21+
// Inside of the SDK, the sysroot directory has the same name as the
22+
// target triple.
23+
let targetTripleString = targetTriple.triple
24+
let properties = SwiftSDKMetadataV4.TripleProperties(
25+
sdkRootPath: targetTripleString,
26+
toolsetPaths: ["toolset.json"]
27+
)
28+
metadata.targetTriples = [
29+
targetTripleString: properties
30+
]
31+
}
32+
33+
package func applyPlatformOptions(toolset: inout Toolset, targetTriple: Triple, isForEmbeddedSwift: Bool
34+
) {
35+
// The toolset data is always the same. It just instructs Swift and LLVM
36+
// to use the LLVM linker lld instead of whatever the system linker is.
37+
// It also instructs the linker to set the runtime paths so that the
38+
// dynamic linker can find the Swift runtime libraries.
39+
let swiftCompilerOptions = Toolset.ToolProperties(extraCLIOptions: ["-Xclang-linker", "-fuse-ld=lld", "-Xclang-linker", "-Wl,-rpath", "-Xclang-linker", "-Wl,/usr/local/swift/lib:/usr/local/swift/lib/swift/freebsd"])
40+
toolset.swiftCompiler = swiftCompilerOptions
41+
}
42+
43+
/// The FreeBSD version that we are targeting.
44+
package let freeBSD: FreeBSD
45+
46+
/// A toolchain compiled for FreeBSD whose contents we should use for the
47+
/// SDK.
48+
///
49+
/// If this is nil, then the resulting SDK won't be able to compile Swift
50+
/// code and will only be able to build code for C and C++.
51+
private let sourceSwiftToolchain: FilePath?
52+
53+
/// The triple of the target architecture that the SDK will support.
54+
private let mainTargetTriple: Triple
55+
56+
/// The target architecture that the SDK will support (e.g., aarch64).
57+
private let architecture: String
58+
59+
/// The default filename of the produced SDK.
60+
package var defaultArtifactID: String {
61+
"""
62+
FreeBSD_\(self.freeBSD.version)_\(self.mainTargetTriple.archName)
63+
"""
64+
}
65+
66+
/// The logging object used by this class for debugging.
67+
package let logger: Logging.Logger
68+
69+
/// Toolchain paths needed for cross-compilation. This is a dictionary that
70+
/// maps paths in the toolchain to the destination path in the SDK. We do
71+
/// this because our packaging script for FreeBSD installs Swift content
72+
/// in /usr/local/swift for ease of packaging, but there's no need to do so
73+
/// in the SDK.
74+
private let neededToolchainPaths = [
75+
"usr/local/swift/lib/swift": "usr/lib/swift",
76+
"usr/local/swift/lib/swift_static": "usr/lib/swift_static",
77+
"usr/local/swift/include/swift": "usr/include/swift",
78+
"usr/local/swift/include": "usr/include/swift",
79+
]
80+
81+
private func baseSysURL() -> String {
82+
// The FreeBSD package system uses arm64 instead of aarch64 in its URLs.
83+
let architectureString: String
84+
if mainTargetTriple.arch == .aarch64 {
85+
architectureString = "arm64"
86+
} else {
87+
architectureString = architecture
88+
}
89+
90+
let majorVersion = freeBSD.majorVersion
91+
let minorVersion = freeBSD.minorVersion
92+
return "https://download.freebsd.org/ftp/releases/\(architectureString)/\(majorVersion).\(minorVersion)-RELEASE/base.txz"
93+
}
94+
95+
package func makeSwiftSDK(generator: SwiftSDKGenerator, engine: Helpers.QueryEngine, httpClient: some HTTPClientProtocol) async throws -> SwiftSDKProduct {
96+
let swiftSDKRootPath = generator.pathsConfiguration.swiftSDKRootPath
97+
try await generator.createDirectoryIfNeeded(at: swiftSDKRootPath)
98+
logger.debug("swiftSDKRootPath = \(swiftSDKRootPath)")
99+
100+
// Create the sysroot directory. This is where all of the FreeBSD content
101+
// as well as the Swift toolchain will be copied to.
102+
let sysrootDir = swiftSDKRootPath.appending(mainTargetTriple.triple)
103+
try await generator.createDirectoryIfNeeded(at: sysrootDir)
104+
logger.debug("sysrootDir = \(sysrootDir)")
105+
106+
let cachePath = generator.pathsConfiguration.artifactsCachePath
107+
logger.debug("cachePath = \(cachePath)")
108+
109+
// Download the FreeBSD base system if we don't have it in the cache.
110+
let freebsdBaseSystemTarballPath = cachePath.appending("FreeBSD-\(freeBSD.version)-base.txz")
111+
let freebsdBaseSystemUrl = URL(string: baseSysURL())!
112+
if await !generator.doesFileExist(at: freebsdBaseSystemTarballPath) {
113+
try await httpClient.downloadFile(from: freebsdBaseSystemUrl, to: freebsdBaseSystemTarballPath)
114+
}
115+
116+
// Extract the FreeBSD base system into the sysroot.
117+
let neededPathsInSysroot = ["lib", "usr/include", "usr/lib"]
118+
try await generator.untar(
119+
file: freebsdBaseSystemTarballPath,
120+
into: sysrootDir,
121+
paths: neededPathsInSysroot,
122+
)
123+
124+
// If the user provided a Swift toolchain, then also copy its contents
125+
// into the sysroot. We don't need the entire toolchain, only the libraries
126+
// and headers.
127+
if let sourceSwiftToolchain {
128+
// If the toolchain is a directory, then we need to expand it.
129+
let pathToCompleteToolchain: FilePath
130+
if await generator.doesDirectoryExist(at: sourceSwiftToolchain) {
131+
pathToCompleteToolchain = sourceSwiftToolchain
132+
} else {
133+
let expandedToolchainName = "ExpandedSwiftToolchain-FreeBSD-\(freeBSD.version)"
134+
pathToCompleteToolchain = cachePath.appending(expandedToolchainName)
135+
136+
if await generator.doesFileExist(at: pathToCompleteToolchain) {
137+
try await generator.removeFile(at: pathToCompleteToolchain)
138+
}
139+
140+
logger.debug("Expanding archived Swift toolchain at \(sourceSwiftToolchain) into \(pathToCompleteToolchain)")
141+
try await generator.createDirectoryIfNeeded(at: pathToCompleteToolchain)
142+
try await generator.untar(
143+
file: sourceSwiftToolchain,
144+
into: pathToCompleteToolchain
145+
)
146+
}
147+
148+
logger.debug("Copying required items from toolchain into SDK")
149+
for (sourcePath, destinationPath) in neededToolchainPaths {
150+
let sourcePath = pathToCompleteToolchain.appending(sourcePath)
151+
let destinationPath = sysrootDir.appending(destinationPath)
152+
153+
logger.debug("Copying item in toolchain at path \(sourcePath) into SDK at \(destinationPath)")
154+
try await generator.createDirectoryIfNeeded(at: destinationPath.removingLastComponent())
155+
try await generator.copy(from: sourcePath, to: destinationPath)
156+
}
157+
}
158+
159+
// Return the path to the newly created SDK.
160+
return .init(sdkDirPath: swiftSDKRootPath, hostTriples: nil)
161+
}
162+
163+
public init(freeBSDVersion: FreeBSD, mainTargetTriple: Triple, sourceSwiftToolchain: FilePath?, logger: Logging.Logger) {
164+
self.freeBSD = freeBSDVersion
165+
self.mainTargetTriple = mainTargetTriple
166+
self.architecture = mainTargetTriple.archName
167+
self.logger = logger
168+
self.sourceSwiftToolchain = sourceSwiftToolchain
169+
}
170+
}

Sources/SwiftSDKGenerator/SystemUtils/GeneratorError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import struct Foundation.URL
1414
import struct SystemPackage.FilePath
1515

1616
enum GeneratorError: Error {
17+
case invalidVersionString(string: String, reason: String)
1718
case noProcessOutput(String)
1819
case unhandledChildProcessSignal(CInt, CommandInfo)
1920
case nonZeroExitCode(CInt, CommandInfo)
21+
case unknownFreebsdVersion(version: String)
2022
case unknownLinuxDistribution(name: String, version: String?)
2123
case unknownMacOSVersion(String)
2224
case unknownCPUArchitecture(String)
@@ -33,12 +35,16 @@ enum GeneratorError: Error {
3335
extension GeneratorError: CustomStringConvertible {
3436
var description: String {
3537
switch self {
38+
case let .invalidVersionString(string: string, reason: reason):
39+
return "The version string '\(string)' is invalid. \(reason)"
3640
case let .noProcessOutput(process):
3741
return "Failed to read standard output of a launched process: \(process)"
3842
case let .unhandledChildProcessSignal(signal, commandInfo):
3943
return "Process launched with \(commandInfo) finished due to signal \(signal)"
4044
case let .nonZeroExitCode(exitCode, commandInfo):
4145
return "Process launched with \(commandInfo) failed with exit code \(exitCode)"
46+
case let .unknownFreebsdVersion(version: version):
47+
return "The string '\(version)' does not represent a valid FreeBSD version."
4248
case let .unknownLinuxDistribution(name, version):
4349
return
4450
"Linux distribution `\(name)`\(version.map { " with version `\($0)`" } ?? "") is not supported by this generator."

0 commit comments

Comments
 (0)