Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended Bundle layout testing #2083

Merged
merged 1 commit into from
Apr 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions CoreFoundation/PlugIn.subproj/CFBundle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
30 changes: 23 additions & 7 deletions Foundation/Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -106,6 +114,14 @@ open class Bundle: NSObject {
_bundle = result
}

public convenience init?(_executableURL: URL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pvieito @millenomi Should this method be public? also _supportsFHSBundles and _supportsFreestandingBundle above? If its just for testing shouldn't they be internal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spevans If we mark it internal we would have to guard the tests with NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT, which means that they would not be executed on the Linux CI (and I would like them to be executed on the Linux CI, as I can only test them locally on macOS).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT works for Linux CI because it uses the debug-foundation flag. @millenomi Is that not the case?

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"))"
}
Expand Down
181 changes: 151 additions & 30 deletions TestFoundation/TestBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want to have some constants for this in FileManager as we keep wanting to do this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea :\ probably a SPI picking up that information from CF (like: Bundle._platformLibraryPrefix/Suffix, Bundle._platformExecutablePrefix/Suffix or similar).

}

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
}
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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
}

Expand All @@ -135,7 +228,7 @@ class BundlePlayground {
return false
}

case .fhsInstalled:
case .fhs(let executableType):
do {

// Create a FHS /usr/local-style hierarchy:
Expand All @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
]
}
Expand Down Expand Up @@ -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
Expand Down