diff --git a/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h b/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h index 5e7cc78be7..f27a0662fe 100644 --- a/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h +++ b/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h @@ -393,6 +393,7 @@ CF_EXPORT _Nullable CFErrorRef CFWriteStreamCopyError(CFWriteStreamRef _Null_uns CF_CROSS_PLATFORM_EXPORT CFStringRef _Nullable _CFBundleCopyExecutablePath(CFBundleRef bundle); CF_CROSS_PLATFORM_EXPORT Boolean _CFBundleSupportsFHSBundles(void); +CF_CROSS_PLATFORM_EXPORT Boolean _CFBundleSupportsFreestandingBundles(void); CF_CROSS_PLATFORM_EXPORT CFStringRef __CFTimeZoneCopyDataVersionString(void); // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/CoreFoundation/PlugIn.subproj/CFBundle.c b/CoreFoundation/PlugIn.subproj/CFBundle.c index 0d3cb32beb..9cba868372 100644 --- a/CoreFoundation/PlugIn.subproj/CFBundle.c +++ b/CoreFoundation/PlugIn.subproj/CFBundle.c @@ -170,6 +170,14 @@ CF_CROSS_PLATFORM_EXPORT Boolean _CFBundleSupportsFHSBundles() { #endif } +CF_CROSS_PLATFORM_EXPORT Boolean _CFBundleSupportsFreestandingBundles() { +#if !DEPLOYMENT_RUNTIME_OBJC + return true; +#else + return false; +#endif +} + #pragma mark - CF_PRIVATE os_log_t _CFBundleResourceLogger(void) { diff --git a/Foundation/Bundle.swift b/Foundation/Bundle.swift index d2fd86f76a..9fc00b094a 100644 --- a/Foundation/Bundle.swift +++ b/Foundation/Bundle.swift @@ -11,13 +11,21 @@ import CoreFoundation open class Bundle: NSObject { private var _bundle : CFBundle! - - internal static var _supportsFHSStyle: Bool { - #if DEPLOYMENT_RUNTIME_OBJC - return false - #else - return _CFBundleSupportsFHSBundles() - #endif + + public static var _supportsFHSBundles: Bool { + #if DEPLOYMENT_RUNTIME_OBJC + return false + #else + return _CFBundleSupportsFHSBundles() + #endif + } + + public static var _supportsFreestandingBundles: Bool { + #if DEPLOYMENT_RUNTIME_OBJC + return false + #else + return _CFBundleSupportsFreestandingBundles() + #endif } private static var _mainBundle : Bundle = { @@ -106,6 +114,14 @@ open class Bundle: NSObject { _bundle = result } + public convenience init?(_executableURL: URL) { + guard let bundleURL = _CFBundleCopyBundleURLForExecutableURL(_executableURL._cfObject)?.takeRetainedValue() else { + return nil + } + + self.init(url: bundleURL._swiftObject) + } + override open var description: String { return "\(String(describing: Bundle.self)) <\(bundleURL.path)> (\(isLoaded ? "loaded" : "not yet loaded"))" } diff --git a/TestFoundation/TestBundle.swift b/TestFoundation/TestBundle.swift index 78e20481cc..ea8bbf122a 100644 --- a/TestFoundation/TestBundle.swift +++ b/TestFoundation/TestBundle.swift @@ -36,26 +36,110 @@ internal func xdgTestHelperURL() -> URL { class BundlePlayground { + enum ExecutableType: CaseIterable { + case library + case executable + + var pathExtension: String { + switch self { + case .library: + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + return "dylib" + #elseif os(Windows) + return "dll" + #else + return "so" + #endif + case .executable: + #if os(Windows) + return "exe" + #else + return "" + #endif + } + } + + var flatPathExtension: String { + #if os(Windows) + return self.pathExtension + #else + return "" + #endif + } + + var fhsPrefix: String { + switch self { + case .executable: + return "bin" + case .library: + return "lib" + } + } + + var nonFlatFilePrefix: String { + switch self { + case .executable: + return "" + case .library: + return "lib" + } + } + } + enum Layout { - case flat - case fhsInstalled - case fhsFreestanding + case flat(ExecutableType) + case fhs(ExecutableType) + case freestanding(ExecutableType) static var allApplicable: [Layout] { - let layouts: [Layout] = [ .flat, .fhsInstalled, .fhsFreestanding ] - -#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT - if Bundle._supportsFHSStyle { - return layouts - } else { - return layouts.filter { !$0.isFHS } + let layouts: [Layout] = [ + .flat(.library), + .flat(.executable), + .fhs(.library), + .fhs(.executable), + .freestanding(.library), + .freestanding(.executable), + ] + + return layouts.filter { $0.isSupported } + } + + var isFreestanding: Bool { + switch self { + case .freestanding(_): + return true + default: + return false } -#else - return layouts.filter { !$0.isFHS } -#endif } + var isFHS: Bool { - return self == .fhsInstalled || self == .fhsFreestanding + switch self { + case .fhs(_): + return true + default: + return false + } + } + + var isFlat: Bool { + switch self { + case .flat(_): + return true + default: + return false + } + } + + var isSupported: Bool { + switch self { + case .flat(_): + return true + case .freestanding(_): + return Bundle._supportsFreestandingBundles + case .fhs(_): + return Bundle._supportsFHSBundles + } } } @@ -68,6 +152,7 @@ class BundlePlayground { let layout: Layout private(set) var bundlePath: String! + private(set) var mainExecutableURL: URL! private var playgroundPath: String? init?(bundleName: String, @@ -96,8 +181,8 @@ class BundlePlayground { let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("TestFoundation_Playground_" + UUID().uuidString) - switch (layout) { - case .flat: + switch layout { + case .flat(let executableType): do { try FileManager.default.createDirectory(atPath: temporaryDirectory.path, withIntermediateDirectories: false, attributes: nil) @@ -106,10 +191,18 @@ class BundlePlayground { try FileManager.default.createDirectory(atPath: bundleURL.path, withIntermediateDirectories: false, attributes: nil) // Make a main and an auxiliary executable: - guard FileManager.default.createFile(atPath: bundleURL.appendingPathComponent(bundleName).path, contents: nil) else { + self.mainExecutableURL = bundleURL + .appendingPathComponent(bundleName) + .appendingPathExtension(executableType.flatPathExtension) + + guard FileManager.default.createFile(atPath: mainExecutableURL.path, contents: nil) else { return false } - guard FileManager.default.createFile(atPath: bundleURL.appendingPathComponent(auxiliaryExecutableName).path, contents: nil) else { + + let auxiliaryExecutableURL = bundleURL + .appendingPathComponent(auxiliaryExecutableName) + .appendingPathExtension(executableType.flatPathExtension) + guard FileManager.default.createFile(atPath: auxiliaryExecutableURL.path, contents: nil) else { return false } @@ -135,7 +228,7 @@ class BundlePlayground { return false } - case .fhsInstalled: + case .fhs(let executableType): do { // Create a FHS /usr/local-style hierarchy: @@ -144,17 +237,18 @@ class BundlePlayground { try FileManager.default.createDirectory(atPath: temporaryDirectory.appendingPathComponent("lib").path, withIntermediateDirectories: false, attributes: nil) // Make a main and an auxiliary executable: - #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) - let pathExtension = "dylib" - #else - let pathExtension = "so" - #endif - - guard FileManager.default.createFile(atPath: temporaryDirectory.appendingPathComponent("lib").appendingPathComponent("lib\(bundleName).\(pathExtension)").path, contents: nil) else { return false } + self.mainExecutableURL = temporaryDirectory + .appendingPathComponent(executableType.fhsPrefix) + .appendingPathComponent(executableType.nonFlatFilePrefix + bundleName) + .appendingPathExtension(executableType.pathExtension) + guard FileManager.default.createFile(atPath: mainExecutableURL.path, contents: nil) else { return false } let executablesDirectory = temporaryDirectory.appendingPathComponent("libexec").appendingPathComponent("\(bundleName).executables") try FileManager.default.createDirectory(atPath: executablesDirectory.path, withIntermediateDirectories: true, attributes: nil) - guard FileManager.default.createFile(atPath: executablesDirectory.appendingPathComponent(auxiliaryExecutableName).path, contents: nil) else { return false } + let auxiliaryExecutableURL = executablesDirectory + .appendingPathComponent(executableType.nonFlatFilePrefix + auxiliaryExecutableName) + .appendingPathExtension(executableType.pathExtension) + guard FileManager.default.createFile(atPath: auxiliaryExecutableURL.path, contents: nil) else { return false } // Make a .resources directory in …/share: let resourcesDirectory = temporaryDirectory.appendingPathComponent("share").appendingPathComponent("\(bundleName).resources") @@ -178,21 +272,27 @@ class BundlePlayground { return false } - case .fhsFreestanding: + case .freestanding(let executableType): do { let bundleName = URL(string:self.bundleName)!.deletingPathExtension().path try FileManager.default.createDirectory(atPath: temporaryDirectory.path, withIntermediateDirectories: false, attributes: nil) // Make a main executable: - guard FileManager.default.createFile(atPath: temporaryDirectory.appendingPathComponent(bundleName).path, contents: nil) else { return false } + self.mainExecutableURL = temporaryDirectory + .appendingPathComponent(executableType.nonFlatFilePrefix + bundleName) + .appendingPathExtension(executableType.pathExtension) + guard FileManager.default.createFile(atPath: mainExecutableURL.path, contents: nil) else { return false } // Make a .resources directory: let resourcesDirectory = temporaryDirectory.appendingPathComponent("\(bundleName).resources") try FileManager.default.createDirectory(atPath: resourcesDirectory.path, withIntermediateDirectories: false, attributes: nil) // Make an auxiliary executable: - guard FileManager.default.createFile(atPath: resourcesDirectory.appendingPathComponent(auxiliaryExecutableName).path, contents: nil) else { return false } + let auxiliaryExecutableURL = resourcesDirectory + .appendingPathComponent(executableType.nonFlatFilePrefix + auxiliaryExecutableName) + .appendingPathExtension(executableType.pathExtension) + guard FileManager.default.createFile(atPath: auxiliaryExecutableURL.path, contents: nil) else { return false } // Put some resources in the bundle for resourceName in resourceFilenames { @@ -244,6 +344,7 @@ class TestBundle : XCTestCase { ("test_bundlePreflight", test_bundlePreflight), ("test_bundleFindExecutable", test_bundleFindExecutable), ("test_bundleFindAuxiliaryExecutables", test_bundleFindAuxiliaryExecutables), + ("test_bundleReverseBundleLookup", test_bundleReverseBundleLookup), ("test_mainBundleExecutableURL", test_mainBundleExecutableURL), ] } @@ -430,6 +531,26 @@ class TestBundle : XCTestCase { XCTAssertNil(bundle.url(forAuxiliaryExecutable: "does_not_exist_at_all")) } } + + func test_bundleReverseBundleLookup() { + _withEachPlaygroundLayout { (playground) in + #if !os(Windows) + if playground.layout.isFreestanding { + // TODO: Freestanding bundles reverse lookup pending to be implemented on non-Windows platforms. + return + } + #endif + + if playground.layout.isFHS { + // TODO: FHS bundles reverse lookup pending to be implemented on all platforms. + return + } + + let bundle = Bundle(_executableURL: playground.mainExecutableURL) + XCTAssertNotNil(bundle) + XCTAssertEqual(bundle?.bundlePath, playground.bundlePath) + } + } func test_mainBundleExecutableURL() { #if !DARWIN_COMPATIBILITY_TESTS // _CFProcessPath() is unavailable on native Foundation