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."