From c18bf5459f992234fb78c3a92ce82c0222e7b7d5 Mon Sep 17 00:00:00 2001 From: CodingCarpincho Date: Thu, 28 Aug 2025 13:14:17 -0700 Subject: [PATCH] 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. --- README.md | 3 +- Sources/GeneratorCLI/GeneratorCLI.swift | 80 +++++++- .../Generator/SwiftSDKGenerator.swift | 20 +- .../PlatformModels/FreeBSD.swift | 52 +++++ .../SwiftSDKRecipes/FreeBSDRecipe.swift | 193 ++++++++++++++++++ .../SystemUtils/GeneratorError.swift | 6 + 6 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 Sources/SwiftSDKGenerator/PlatformModels/FreeBSD.swift create mode 100644 Sources/SwiftSDKGenerator/SwiftSDKRecipes/FreeBSDRecipe.swift diff --git a/README.md b/README.md index 6dc9f89..17897b3 100644 --- a/README.md +++ b/README.md @@ -42,13 +42,14 @@ brew bundle install ## Supported platforms and minimum versions -macOS as a host platform and Linux as both host and target platforms are supported by the generator. +FreeBSD and Linux are supported as both host and target platforms. macOS is only supported as a host platform. The generator also allows cross-compiling between any Linux distributions officially supported by the Swift project. | Platform | Supported Version as Host | Supported Version as Target | | -: | :- | :- | | macOS (arm64) | ✅ macOS 13.0+ | ❌ | | macOS (x86_64) | ✅ macOS 13.0+[^1] | ❌ | +| FreeBSD | ✅ 14.3+ | ✅ 14.3+ | | Ubuntu | ✅ 20.04+ | ✅ 20.04+ | | Debian | ✅ 11, 12[^2] | ✅ 11, 12[^2] | | RHEL | ✅ Fedora 39, UBI 9 | ✅ Fedora 39, UBI 9[^3] | diff --git a/Sources/GeneratorCLI/GeneratorCLI.swift b/Sources/GeneratorCLI/GeneratorCLI.swift index d1a5432..6f5b24a 100644 --- a/Sources/GeneratorCLI/GeneratorCLI.swift +++ b/Sources/GeneratorCLI/GeneratorCLI.swift @@ -23,7 +23,7 @@ struct GeneratorCLI: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "swift-sdk-generator", - subcommands: [MakeLinuxSDK.self, MakeWasmSDK.self], + subcommands: [MakeLinuxSDK.self, MakeFreeBSDSDK.self, MakeWasmSDK.self], defaultSubcommand: MakeLinuxSDK.self ) @@ -304,6 +304,84 @@ extension GeneratorCLI { } } + struct MakeFreeBSDSDK: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "make-freebsd-sdk", + abstract: "Generate a Swift SDK bundle for FreeBSD.", + discussion: """ + The default `--target` triple is FreeBSD for x86_64 + """ + ) + + @OptionGroup + var generatorOptions: GeneratorOptions + + @Option( + help: """ + Swift toolchain for FreeBSD from which to copy the Swift libraries. \ + This must match the architecture of the target triple. + """ + ) + var fromSwiftToolchain: String? = nil + + @Option( + help: """ + Version of FreeBSD to use as a target platform. Example: 14.3 + """ + ) + var freeBSDVersion: String + + func deriveTargetTriple() throws -> Triple { + if let target = generatorOptions.target, target.os == .freeBSD { + return target + } + if let arch = generatorOptions.targetArch { + let target = Triple(arch: arch, vendor: nil, os: .freeBSD) + appLogger.warning( + """ + Using `--target-arch \(arch)` defaults to `\(target.triple)`. \ + Use `--target` if you want to pass the full target triple. + """ + ) + return target + } else { + // If no target is given, use the architecture of the host. + let hostTriple = try self.generatorOptions.deriveHostTriple() + let hostArch = hostTriple.arch! + return Triple(arch: hostArch, vendor: nil, os: .freeBSD) + } + } + + func run() async throws { + let freebsdVersion = try FreeBSD(self.freeBSDVersion) + guard freebsdVersion.isSupportedVersion() else { + throw StringError("Only FreeBSD versions 14.3 or higher are supported.") + } + + let hostTriple = try self.generatorOptions.deriveHostTriple() + let targetTriple = try self.deriveTargetTriple() + + let sourceSwiftToolchain: FilePath? + if let fromSwiftToolchain { + sourceSwiftToolchain = .init(fromSwiftToolchain) + } else { + sourceSwiftToolchain = nil + } + + let recipe = FreeBSDRecipe( + freeBSDVersion: freebsdVersion, + mainTargetTriple: targetTriple, + sourceSwiftToolchain: sourceSwiftToolchain, + logger: loggerWithLevel(from: self.generatorOptions) + ) + try await GeneratorCLI.run( + recipe: recipe, + targetTriple: targetTriple, + options: self.generatorOptions + ) + } + } + struct MakeWasmSDK: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "make-wasm-sdk", diff --git a/Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator.swift b/Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator.swift index 2839a82..bbd06c9 100644 --- a/Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator.swift +++ b/Sources/SwiftSDKGenerator/Generator/SwiftSDKGenerator.swift @@ -149,6 +149,12 @@ public actor SwiftSDKGenerator { self.fileManager.fileExists(atPath: path.string) } + func doesDirectoryExist(at path: FilePath) -> Bool { + var isDirectory: ObjCBool = false + let itemExists = self.fileManager.fileExists(atPath: path.string, isDirectory: &isDirectory) + return itemExists && isDirectory.boolValue + } + func removeFile(at path: FilePath) throws { try self.fileManager.removeItem(atPath: path.string) } @@ -245,7 +251,8 @@ public actor SwiftSDKGenerator { func untar( file: FilePath, into directoryPath: FilePath, - stripComponents: Int? = nil + stripComponents: Int? = nil, + paths: [String]? = nil ) async throws { let stripComponentsOption: String if let stripComponents { @@ -253,8 +260,15 @@ public actor SwiftSDKGenerator { } else { stripComponentsOption = "" } + + var command = #"tar -C "\#(directoryPath)" \#(stripComponentsOption) -xf "\#(file)""# + if let paths { + for path in paths { + command += #" "\#(path)""# + } + } try await Shell.run( - #"tar -C "\#(directoryPath)" \#(stripComponentsOption) -xf "\#(file)""#, + command, shouldLogCommands: self.isVerbose ) } @@ -298,6 +312,8 @@ public actor SwiftSDKGenerator { } else { try await self.gunzip(file: file, into: directoryPath) } + case "txz": + try await self.untar(file: file, into: directoryPath) case "deb": try await self.unpack(debFile: file, into: directoryPath) case "pkg": diff --git a/Sources/SwiftSDKGenerator/PlatformModels/FreeBSD.swift b/Sources/SwiftSDKGenerator/PlatformModels/FreeBSD.swift new file mode 100644 index 0000000..b5289ca --- /dev/null +++ b/Sources/SwiftSDKGenerator/PlatformModels/FreeBSD.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +public struct FreeBSD: Sendable { + public let version: String + + public let majorVersion: Int + public let minorVersion: Int + + public func isSupportedVersion() -> Bool { + if majorVersion >= 15 { + return true + } else if majorVersion == 14, minorVersion >= 3 { + return true + } else { + return false + } + } + + public init(_ versionString: String) throws { + guard !versionString.isEmpty else { + throw GeneratorError.invalidVersionString( + string: versionString, + reason: "The version string cannot be empty." + ) + } + + let versionComponents = versionString.split(separator: ".") + guard let majorVersion = Int(versionComponents[0]) else { + throw GeneratorError.unknownFreeBSDVersion(version: versionString) + } + + self.version = versionString + self.majorVersion = majorVersion + if versionComponents.count > 1, let minorVersion = Int(versionComponents[1]) { + self.minorVersion = minorVersion + } else { + minorVersion = 0 + } + } +} diff --git a/Sources/SwiftSDKGenerator/SwiftSDKRecipes/FreeBSDRecipe.swift b/Sources/SwiftSDKGenerator/SwiftSDKRecipes/FreeBSDRecipe.swift new file mode 100644 index 0000000..1595e53 --- /dev/null +++ b/Sources/SwiftSDKGenerator/SwiftSDKRecipes/FreeBSDRecipe.swift @@ -0,0 +1,193 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation +import Helpers +import Logging + +import struct SystemPackage.FilePath + +package struct FreeBSDRecipe: SwiftSDKRecipe { + package func applyPlatformOptions( + metadata: inout SwiftSDKMetadataV4, + paths: PathsConfiguration, + targetTriple: Triple, + isForEmbeddedSwift: Bool + ) { + // Inside of the SDK, the sysroot directory has the same name as the + // target triple. + let targetTripleString = targetTriple.triple + let properties = SwiftSDKMetadataV4.TripleProperties( + sdkRootPath: targetTripleString, + toolsetPaths: ["toolset.json"] + ) + metadata.targetTriples = [ + targetTripleString: properties + ] + } + + package func applyPlatformOptions( + toolset: inout Toolset, + targetTriple: Triple, + isForEmbeddedSwift: Bool + ) { + // The toolset data is always the same. It just instructs Swift and LLVM + // to use the LLVM linker lld instead of whatever the system linker is. + // It also instructs the linker to set the runtime paths so that the + // dynamic linker can find the Swift runtime libraries. + 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", + ] + ) + toolset.swiftCompiler = swiftCompilerOptions + } + + /// The FreeBSD version that we are targeting. + package let freeBSD: FreeBSD + + /// A toolchain compiled for FreeBSD whose contents we should use for the + /// SDK. + /// + /// If this is nil, then the resulting SDK won't be able to compile Swift + /// code and will only be able to build code for C and C++. + private let sourceSwiftToolchain: FilePath? + + /// The triple of the target architecture that the SDK will support. + private let mainTargetTriple: Triple + + /// The target architecture that the SDK will support (e.g., aarch64). + private let architecture: String + + /// The default filename of the produced SDK. + package var defaultArtifactID: String { + """ + FreeBSD_\(self.freeBSD.version)_\(self.mainTargetTriple.archName) + """ + } + + /// The logging object used by this class for debugging. + package let logger: Logging.Logger + + /// Toolchain paths needed for cross-compilation. This is a dictionary that + /// maps paths in the toolchain to the destination path in the SDK. We do + /// this because our packaging script for FreeBSD installs Swift content + /// in /usr/local/swift for ease of packaging, but there's no need to do so + /// in the SDK. + private let neededToolchainPaths = [ + "usr/local/swift/lib/swift": "usr/lib/swift", + "usr/local/swift/lib/swift_static": "usr/lib/swift_static", + "usr/local/swift/include/swift": "usr/include/swift", + "usr/local/swift/include": "usr/include/swift", + ] + + private func baseSysURL() -> String { + // The FreeBSD package system uses arm64 instead of aarch64 in its URLs. + let architectureString: String + if mainTargetTriple.arch == .aarch64 { + architectureString = "arm64" + } else { + architectureString = architecture + } + + let majorVersion = freeBSD.majorVersion + let minorVersion = freeBSD.minorVersion + return + "https://download.freebsd.org/ftp/releases/\(architectureString)/\(majorVersion).\(minorVersion)-RELEASE/base.txz" + } + + package func makeSwiftSDK( + generator: SwiftSDKGenerator, + engine: Helpers.QueryEngine, + httpClient: some HTTPClientProtocol + ) async throws -> SwiftSDKProduct { + let swiftSDKRootPath = generator.pathsConfiguration.swiftSDKRootPath + try await generator.createDirectoryIfNeeded(at: swiftSDKRootPath) + logger.debug("swiftSDKRootPath = \(swiftSDKRootPath)") + + // Create the sysroot directory. This is where all of the FreeBSD content + // as well as the Swift toolchain will be copied to. + let sysrootDir = swiftSDKRootPath.appending(mainTargetTriple.triple) + try await generator.createDirectoryIfNeeded(at: sysrootDir) + logger.debug("sysrootDir = \(sysrootDir)") + + let cachePath = generator.pathsConfiguration.artifactsCachePath + logger.debug("cachePath = \(cachePath)") + + // Download the FreeBSD base system if we don't have it in the cache. + let freeBSDBaseSystemTarballPath = cachePath.appending("FreeBSD-\(freeBSD.version)-base.txz") + let freebsdBaseSystemUrl = URL(string: baseSysURL())! + if await !generator.doesFileExist(at: freeBSDBaseSystemTarballPath) { + try await httpClient.downloadFile(from: freebsdBaseSystemUrl, to: freeBSDBaseSystemTarballPath) + } + + // Extract the FreeBSD base system into the sysroot. + let neededPathsInSysroot = ["lib", "usr/include", "usr/lib"] + try await generator.untar( + file: freeBSDBaseSystemTarballPath, + into: sysrootDir, + paths: neededPathsInSysroot + ) + + // If the user provided a Swift toolchain, then also copy its contents + // into the sysroot. We don't need the entire toolchain, only the libraries + // and headers. + if let sourceSwiftToolchain { + // If the toolchain is a directory, then we need to expand it. + let pathToCompleteToolchain: FilePath + if await generator.doesDirectoryExist(at: sourceSwiftToolchain) { + pathToCompleteToolchain = sourceSwiftToolchain + } else { + let expandedToolchainName = "ExpandedSwiftToolchain-FreeBSD-\(freeBSD.version)" + pathToCompleteToolchain = cachePath.appending(expandedToolchainName) + + if await generator.doesFileExist(at: pathToCompleteToolchain) { + try await generator.removeFile(at: pathToCompleteToolchain) + } + + logger.debug("Expanding archived Swift toolchain at \(sourceSwiftToolchain) into \(pathToCompleteToolchain)") + try await generator.createDirectoryIfNeeded(at: pathToCompleteToolchain) + try await generator.untar( + file: sourceSwiftToolchain, + into: pathToCompleteToolchain + ) + } + + logger.debug("Copying required items from toolchain into SDK") + for (sourcePath, destinationPath) in neededToolchainPaths { + let sourcePath = pathToCompleteToolchain.appending(sourcePath) + let destinationPath = sysrootDir.appending(destinationPath) + + logger.debug("Copying item in toolchain at path \(sourcePath) into SDK at \(destinationPath)") + try await generator.createDirectoryIfNeeded(at: destinationPath.removingLastComponent()) + try await generator.copy(from: sourcePath, to: destinationPath) + } + } + + // Return the path to the newly created SDK. + return .init(sdkDirPath: swiftSDKRootPath, hostTriples: nil) + } + + public init( + freeBSDVersion: FreeBSD, + mainTargetTriple: Triple, + sourceSwiftToolchain: FilePath?, + logger: Logging.Logger + ) { + self.freeBSD = freeBSDVersion + self.mainTargetTriple = mainTargetTriple + self.architecture = mainTargetTriple.archName + self.logger = logger + self.sourceSwiftToolchain = sourceSwiftToolchain + } +} diff --git a/Sources/SwiftSDKGenerator/SystemUtils/GeneratorError.swift b/Sources/SwiftSDKGenerator/SystemUtils/GeneratorError.swift index 10071c4..b894528 100644 --- a/Sources/SwiftSDKGenerator/SystemUtils/GeneratorError.swift +++ b/Sources/SwiftSDKGenerator/SystemUtils/GeneratorError.swift @@ -14,9 +14,11 @@ import struct Foundation.URL import struct SystemPackage.FilePath enum GeneratorError: Error { + case invalidVersionString(string: String, reason: String) case noProcessOutput(String) case unhandledChildProcessSignal(CInt, CommandInfo) case nonZeroExitCode(CInt, CommandInfo) + case unknownFreeBSDVersion(version: String) case unknownLinuxDistribution(name: String, version: String?) case unknownMacOSVersion(String) case unknownCPUArchitecture(String) @@ -33,12 +35,16 @@ enum GeneratorError: Error { extension GeneratorError: CustomStringConvertible { var description: String { switch self { + case let .invalidVersionString(string: string, reason: reason): + return "The version string '\(string)' is invalid. \(reason)" case let .noProcessOutput(process): return "Failed to read standard output of a launched process: \(process)" case let .unhandledChildProcessSignal(signal, commandInfo): return "Process launched with \(commandInfo) finished due to signal \(signal)" case let .nonZeroExitCode(exitCode, commandInfo): return "Process launched with \(commandInfo) failed with exit code \(exitCode)" + case let .unknownFreeBSDVersion(version: version): + return "The string '\(version)' does not represent a valid FreeBSD version." case let .unknownLinuxDistribution(name, version): return "Linux distribution `\(name)`\(version.map { " with version `\($0)`" } ?? "") is not supported by this generator."