diff --git a/Package.swift b/Package.swift index 2cbc3eff..d368e124 100644 --- a/Package.swift +++ b/Package.swift @@ -110,7 +110,7 @@ let package = Package( "SWBBuildSystem", "SWBServiceCore", "SWBTaskExecution", - .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .android, .windows])), + .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])), ], exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings(languageMode: .v5)), @@ -201,7 +201,7 @@ let package = Package( "SWBCSupport", "SWBLibc", .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .android, .windows])), + .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])), ], exclude: ["CMakeLists.txt"], swiftSettings: swiftSettings(languageMode: .v5)), diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift index 0bfabcf0..70d95c0d 100644 --- a/Sources/SWBCore/Settings/Settings.swift +++ b/Sources/SWBCore/Settings/Settings.swift @@ -5307,6 +5307,10 @@ extension OperatingSystem { return "windows" case .linux: return "linux" + case .freebsd: + return "freebsd" + case .openbsd: + return "openbsd" case .android: return "android" case .unknown: diff --git a/Sources/SWBGenericUnixPlatform/Plugin.swift b/Sources/SWBGenericUnixPlatform/Plugin.swift index 128fde75..a3839068 100644 --- a/Sources/SWBGenericUnixPlatform/Plugin.swift +++ b/Sources/SWBGenericUnixPlatform/Plugin.swift @@ -39,7 +39,11 @@ struct GenericUnixPlatformSpecsExtension: SpecificationsExtension { } func specificationDomains() -> [String: [String]] { - ["linux": ["generic-unix"]] + [ + "linux": ["generic-unix"], + "freebsd": ["generic-unix"], + "openbsd": ["generic-unix"], + ] } } @@ -73,9 +77,9 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension { let defaultProperties: [String: PropertyListItem] switch operatingSystem { - case .linux: + case .linux, .freebsd: defaultProperties = [ - // Workaround to avoid `-dependency_info` on Linux. + // Workaround to avoid `-dependency_info`. "LD_DEPENDENCY_INFO_FILE": .plString(""), "GENERATE_TEXT_BASED_STUBS": "NO", @@ -167,6 +171,6 @@ struct GenericUnixToolchainRegistryExtension: ToolchainRegistryExtension { extension OperatingSystem { /// Whether the Core is allowed to create a fallback toolchain, SDK, and platform for this operating system in cases where no others have been provided. var createFallbackSystemToolchain: Bool { - return self == .linux + return self == .linux || self == .freebsd || self == .openbsd } } diff --git a/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec b/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec new file mode 100644 index 00000000..8d247c89 --- /dev/null +++ b/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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 http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +( + { + Domain = freebsd; + Identifier = com.apple.pbx.linkers.libtool; + BasedOn = generic-unix:com.apple.pbx.linkers.libtool; + Type = Linker; + Options = ( + { + Name = "LIBTOOL_USE_RESPONSE_FILE"; + Type = Boolean; + DefaultValue = NO; + }, + ); + }, +) diff --git a/Sources/SWBTestSupport/RunDestinationTestSupport.swift b/Sources/SWBTestSupport/RunDestinationTestSupport.swift index 9f6ae3bf..247397b7 100644 --- a/Sources/SWBTestSupport/RunDestinationTestSupport.swift +++ b/Sources/SWBTestSupport/RunDestinationTestSupport.swift @@ -98,6 +98,10 @@ extension _RunDestinationInfo { windows case .linux: linux + case .freebsd: + freebsd + case .openbsd: + openbsd case .android: android case .unknown: @@ -259,6 +263,22 @@ extension _RunDestinationInfo { return .init(platform: "linux", sdk: "linux", sdkVariant: "linux", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false) } + /// A run destination targeting FreeBSD generic device, using the public SDK. + package static var freebsd: Self { + guard let arch = Architecture.hostStringValue else { + preconditionFailure("Unknown architecture \(Architecture.host.stringValue ?? "")") + } + return .init(platform: "freebsd", sdk: "freebsd", sdkVariant: "freebsd", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false) + } + + /// A run destination targeting OpenBSD generic device, using the public SDK. + package static var openbsd: Self { + guard let arch = Architecture.hostStringValue else { + preconditionFailure("Unknown architecture \(Architecture.host.stringValue ?? "")") + } + return .init(platform: "openbsd", sdk: "openbsd", sdkVariant: "openbsd", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false) + } + /// A run destination targeting Android generic device, using the public SDK. package static var android: Self { return .init(platform: "android", sdk: "android", sdkVariant: "android", targetArchitecture: "undefined_arch", supportedArchitectures: ["armv7", "aarch64", "riscv64", "i686", "x86_64"], disableOnlyActiveArch: true) diff --git a/Sources/SWBTestSupport/SkippedTestSupport.swift b/Sources/SWBTestSupport/SkippedTestSupport.swift index 27040e16..bef3a66e 100644 --- a/Sources/SWBTestSupport/SkippedTestSupport.swift +++ b/Sources/SWBTestSupport/SkippedTestSupport.swift @@ -49,6 +49,10 @@ extension KnownSDK { return windows case .success(.linux): return linux + case .success(.freebsd): + return freebsd + case .success(.openbsd): + return openbsd case .success(.android): return android case .success(.unknown), .failure: @@ -69,6 +73,8 @@ extension KnownSDK { extension KnownSDK { package static let windows: Self = "windows" package static let linux: Self = "linux" + package static let freebsd: Self = "freebsd" + package static let openbsd: Self = "openbsd" package static let android: Self = "android" package static let qnx: Self = "qnx" package static let wasi: Self = "wasi" @@ -196,7 +202,7 @@ extension Trait where Self == Testing.ConditionTrait { } } - package static func requireSystemPackages(apt: String..., yum: String..., sourceLocation: SourceLocation = #_sourceLocation) -> Self { + package static func requireSystemPackages(apt: String..., yum: String..., freebsd: String..., sourceLocation: SourceLocation = #_sourceLocation) -> Self { enabled("required system packages are not installed") { func checkInstalled(hostOS: OperatingSystem, packageManagerPath: Path, args: [String], packages: [String], regex: Regex<(Substring, name: Substring)>) async throws -> Bool { if try ProcessInfo.processInfo.hostOperatingSystem() == hostOS && localFS.exists(packageManagerPath) { @@ -222,7 +228,9 @@ extension Trait where Self == Testing.ConditionTrait { // spelled `--installed` in newer versions of yum, but Amazon Linux 2 is on older versions let yum = try await checkInstalled(hostOS: .linux, packageManagerPath: Path("/usr/bin/yum"), args: ["list", "installed", "yum"], packages: yum, regex: #/(?.+)\./#) - return apt && yum + let freebsd = try await checkInstalled(hostOS: .freebsd, packageManagerPath: Path("/usr/sbin/pkg"), args: ["info"], packages: freebsd, regex: #/^Name(?:[ ]+): (?.+)$/#) + + return apt && yum && freebsd } } diff --git a/Sources/SWBUtil/Architecture.swift b/Sources/SWBUtil/Architecture.swift index 340491bd..d7518ec8 100644 --- a/Sources/SWBUtil/Architecture.swift +++ b/Sources/SWBUtil/Architecture.swift @@ -98,7 +98,18 @@ public struct Architecture: Sendable { if uname(&buf) == 0 { return withUnsafeBytes(of: &buf.machine) { buf in let data = Data(buf) - return String(decoding: data[0...(data.lastIndex(where: { $0 != 0 }) ?? 0)], as: UTF8.self) + let value = String(decoding: data[0...(data.lastIndex(where: { $0 != 0 }) ?? 0)], as: UTF8.self) + #if os(FreeBSD) + switch value { + case "amd64": + return "x86_64" + case "arm64": + return "aarch64" + default: + break + } + #endif + return value } } return nil diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift index adf69e3a..9f0eb4c2 100644 --- a/Sources/SWBUtil/FSProxy.swift +++ b/Sources/SWBUtil/FSProxy.swift @@ -718,6 +718,9 @@ class LocalFS: FSProxy, @unchecked Sendable { #if os(Windows) // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166 return [] + #elseif os(FreeBSD) + // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836 + return [] #elseif os(OpenBSD) // OpenBSD no longer supports extended attributes return [] @@ -758,6 +761,8 @@ class LocalFS: FSProxy, @unchecked Sendable { func setExtendedAttribute(_ path: Path, key: String, value: ByteString) throws { #if os(Windows) // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166 + #elseif os(FreeBSD) + // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836 #elseif os(OpenBSD) // OpenBSD no longer supports extended attributes #else @@ -778,6 +783,9 @@ class LocalFS: FSProxy, @unchecked Sendable { #if os(Windows) // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166 return nil + #elseif os(FreeBSD) + // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836 + return nil #elseif os(OpenBSD) // OpenBSD no longer supports extended attributes return nil diff --git a/Sources/SWBUtil/Lock.swift b/Sources/SWBUtil/Lock.swift index abc1664a..b45625c6 100644 --- a/Sources/SWBUtil/Lock.swift +++ b/Sources/SWBUtil/Lock.swift @@ -28,7 +28,7 @@ public final class Lock: @unchecked Sendable { #if os(Windows) @usableFromInline let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) - #elseif os(OpenBSD) + #elseif os(FreeBSD) || os(OpenBSD) @usableFromInline let mutex: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: 1) #else diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift index d07f8e9f..d832eb5a 100644 --- a/Sources/SWBUtil/Process.swift +++ b/Sources/SWBUtil/Process.swift @@ -70,6 +70,8 @@ extension Process { case .linux: // Amazon Linux 2 has glibc 2.26, and glibc 2.29 is needed for posix_spawn_file_actions_addchdir_np support FileManager.default.contents(atPath: "/etc/system-release").map { String(decoding: $0, as: UTF8.self) == "Amazon Linux release 2 (Karoo)\n" } ?? false + case .openbsd: + true default: false } diff --git a/Sources/SWBUtil/ProcessInfo.swift b/Sources/SWBUtil/ProcessInfo.swift index 85b4ef2c..e96f8631 100644 --- a/Sources/SWBUtil/ProcessInfo.swift +++ b/Sources/SWBUtil/ProcessInfo.swift @@ -99,6 +99,10 @@ extension ProcessInfo { return .windows #elseif os(Linux) return .linux + #elseif os(FreeBSD) + return .freebsd + #elseif os(OpenBSD) + return .openbsd #else if try FileManager.default.isReadableFile(atPath: systemVersionPlistURL.filePath.str) { switch try systemVersion().productName { @@ -129,6 +133,8 @@ public enum OperatingSystem: Hashable, Sendable { case visionOS(simulator: Bool) case windows case linux + case freebsd + case openbsd case android case unknown @@ -157,7 +163,7 @@ public enum OperatingSystem: Hashable, Sendable { return .macho case .windows: return .pe - case .linux, .android, .unknown: + case .linux, .freebsd, .openbsd, .android, .unknown: return .elf } } diff --git a/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift b/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift index 17f091fa..aa8390b6 100644 --- a/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift +++ b/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift @@ -308,7 +308,7 @@ fileprivate struct BuildTaskBehaviorTests: CoreBasedTests { } /// Check that we honor specs which are unsafe to interrupt. - @Test(.requireSDKs(.host), .skipHostOS(.windows, "no bash shell")) + @Test(.requireSDKs(.host), .skipHostOS(.windows, "no bash shell"), .skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func unsafeToInterrupt() async throws { let fs = localFS let output = MakePlannedVirtualNode("") diff --git a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift index 169c89c7..1b541d75 100644 --- a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift +++ b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift @@ -242,7 +242,7 @@ import SWBMacro } } - @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool")) + @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool", freebsd: "libtool")) func discoveredLibtoolSpecInfo() async throws { try await withSpec(LibtoolLinkerSpec.self, .deferred) { (info: DiscoveredLibtoolLinkerToolSpecInfo) in #expect(info.toolPath.basename == "libtool") diff --git a/Tests/SWBCoreTests/FileTextEncodingTests.swift b/Tests/SWBCoreTests/FileTextEncodingTests.swift index 190dca66..ee898097 100644 --- a/Tests/SWBCoreTests/FileTextEncodingTests.swift +++ b/Tests/SWBCoreTests/FileTextEncodingTests.swift @@ -26,8 +26,7 @@ import SWBTestSupport #expect(FileTextEncoding("utf8") != FileTextEncoding.utf8) } - @Test(.skipHostOS(.windows, "feature not available on Windows due to missing CF APIs"), - .skipHostOS(.linux, "feature not available on Linux due to missing CF APIs")) + @Test(.requireHostOS(.macOS)) // requires CoreFoundation which is macOS-only func encoding() throws { #expect(FileTextEncoding.utf8.stringEncoding == String.Encoding.utf8) #expect(FileTextEncoding.utf16.stringEncoding == String.Encoding.utf16) diff --git a/Tests/SWBCoreTests/SettingsTests.swift b/Tests/SWBCoreTests/SettingsTests.swift index 435100e8..b32b506f 100644 --- a/Tests/SWBCoreTests/SettingsTests.swift +++ b/Tests/SWBCoreTests/SettingsTests.swift @@ -1773,7 +1773,7 @@ import SWBMacro #expect(!core.platformRegistry.platforms.isEmpty) for developmentTeam in ["ABCDWXYZ", ""] { for platform in core.platformRegistry.platforms { - if ["android", "linux", "qnx", "windows"].contains(platform.name) { + if ["android", "freebsd", "linux", "qnx", "windows"].contains(platform.name) { continue } for sdk in platform.sdks { diff --git a/Tests/SWBTaskExecutionTests/PBXCpTests.swift b/Tests/SWBTaskExecutionTests/PBXCpTests.swift index 109bba4d..cfb37a75 100644 --- a/Tests/SWBTaskExecutionTests/PBXCpTests.swift +++ b/Tests/SWBTaskExecutionTests/PBXCpTests.swift @@ -553,7 +553,7 @@ fileprivate struct PBXCpTests: CoreBasedTests { } } - @Test + @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func skipCopyIfContentsEqual() async throws { try await withTemporaryDirectory { tmp in let src = tmp.join("src") diff --git a/Tests/SWBUtilTests/ElapsedTimerTests.swift b/Tests/SWBUtilTests/ElapsedTimerTests.swift index e9f6dab6..3c364148 100644 --- a/Tests/SWBUtilTests/ElapsedTimerTests.swift +++ b/Tests/SWBUtilTests/ElapsedTimerTests.swift @@ -13,9 +13,10 @@ import Foundation import SWBUtil import Testing +import SWBTestSupport @Suite fileprivate struct ElapsedTimerTests { - @Test + @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func time() async throws { do { let delta = try await ElapsedTimer.measure { diff --git a/Tests/SWBUtilTests/FSProxyTests.swift b/Tests/SWBUtilTests/FSProxyTests.swift index 557a33d1..1f56717d 100644 --- a/Tests/SWBUtilTests/FSProxyTests.swift +++ b/Tests/SWBUtilTests/FSProxyTests.swift @@ -488,7 +488,7 @@ import SWBTestSupport case .android, .linux: // This will _usually_ be correct on Linux-derived OSes (see above), but not always. #expect(current_gid == ownership.group) - case .macOS, .iOS, .tvOS, .watchOS, .visionOS: + case .macOS, .iOS, .tvOS, .watchOS, .visionOS, .freebsd, .openbsd: #expect(parentDirOwnership.group == ownership.group) case .windows: // POSIX permissions don't exist, so everything is hardcoded to zero. @@ -566,7 +566,7 @@ import SWBTestSupport } } - @Test(.skipHostOS(.windows)) + @Test(.skipHostOS(.windows), .skipHostOS(.freebsd, "Blocked on https://github.com/swiftlang/swift/pull/77836")) func extendedAttributesSupport() throws { try withTemporaryDirectory { (tmpDir: Path) in // Many filesystems on other platforms (e.g. various non-ext4 temporary filesystems on Linux) don't support xattrs and will return ENOTSUP. diff --git a/Tests/SWBUtilTests/FileHandleTests.swift b/Tests/SWBUtilTests/FileHandleTests.swift index e118f507..51b504b3 100644 --- a/Tests/SWBUtilTests/FileHandleTests.swift +++ b/Tests/SWBUtilTests/FileHandleTests.swift @@ -22,7 +22,7 @@ import SystemPackage #endif @Suite fileprivate struct FileHandleTests { - @Test + @Test(.skipHostOS(.freebsd, "Currently crashes on FreeBSD")) func asyncReadFileDescriptor() async throws { let fs = localFS try await withTemporaryDirectory(fs: fs) { testDataPath in diff --git a/Tests/SWBUtilTests/HeavyCacheTests.swift b/Tests/SWBUtilTests/HeavyCacheTests.swift index 49c496b4..e4f0c9fb 100644 --- a/Tests/SWBUtilTests/HeavyCacheTests.swift +++ b/Tests/SWBUtilTests/HeavyCacheTests.swift @@ -14,6 +14,7 @@ import Foundation import Testing @_spi(Testing) import SWBUtil import Synchronization +import SWBTestSupport @Suite fileprivate struct HeavyCacheTests { @@ -105,7 +106,7 @@ fileprivate struct HeavyCacheTests { } /// Check initial TTL. - @Test + @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func TTL_initial() async throws { let fudgeFactor = 10.0 let ttl = Duration.seconds(0.01) @@ -124,7 +125,7 @@ fileprivate struct HeavyCacheTests { } /// Check TTL set after the fact. - @Test + @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) func TTL_after() async throws { let fudgeFactor = 10.0 let ttl = Duration.seconds(0.01) diff --git a/Tests/SWBUtilTests/MiscTests.swift b/Tests/SWBUtilTests/MiscTests.swift index 36240ad9..f93094a4 100644 --- a/Tests/SWBUtilTests/MiscTests.swift +++ b/Tests/SWBUtilTests/MiscTests.swift @@ -25,7 +25,7 @@ import SWBUtil #expect(SWBUtil.userCacheDir().str.hasPrefix("/var/folders")) case .android: #expect(SWBUtil.userCacheDir().str.hasPrefix("/data/local/tmp")) - case .linux, .unknown: + case .linux, .freebsd, .openbsd, .unknown: #expect(SWBUtil.userCacheDir().str.hasPrefix("/tmp")) } } diff --git a/Tests/SWBUtilTests/RateLimiterTests.swift b/Tests/SWBUtilTests/RateLimiterTests.swift index 932b94fb..3affb9b4 100644 --- a/Tests/SWBUtilTests/RateLimiterTests.swift +++ b/Tests/SWBUtilTests/RateLimiterTests.swift @@ -13,8 +13,10 @@ import Foundation import Testing import SWBUtil +import SWBTestSupport -@Suite fileprivate struct RateLimiterTests { +@Suite(.skipHostOS(.freebsd, "Currently hangs on FreeBSD")) +fileprivate struct RateLimiterTests { @Test func rateLimiterSeconds() async throws { let timer = ElapsedTimer()