Skip to content
Open
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
2 changes: 2 additions & 0 deletions Documentation/xtool.docc/Appex.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,6 @@ struct HelloWidget: Widget {

> Note: Blindly refreshing once an hour isn't a great strategy in practice but it makes for a short snippet. I never said this was a tutorial on writing _good_ WidgetKit code; there's plenty of other resources online if that's your goal, including Apple's own [documentation](https://developer.apple.com/documentation/widgetkit).

> Note: xtool links the extension with `_NSExtensionMain` as the entry point and, unless the extension declares `NSExtensionPrincipalClass` or `NSExtensionMainStoryboard` in its `NSExtension` dictionary, forces the linker to keep the `main` symbol (`-u _main`). Such an extension (for example WidgetKit) must define one in its library: a `@main` type, as above, or top-level code, otherwise it fails to link with an undefined `_main` symbol. An extension with a principal class or main storyboard is instantiated by the runtime and needs no `main`.

Finally, build and run with `xtool dev`. You should be able to locate the widget in your widget library and add it to your home screen!
53 changes: 39 additions & 14 deletions Sources/PackLib/Packer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,27 +233,52 @@ public struct Packer: Sendable {
}

extension Plan.Product {
fileprivate var linkerSettings: String {
/// The `-Xlinker` and friends passed through `unsafeFlags` for this product.
var linkerFlags: [String] {
switch self.type {
case .application: """
case .application: [
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
]
case .appExtension:
// Set the entry point to Foundation`_NSExtensionMain
["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"]
// Keep `main`: nothing references it when the entry point is
// _NSExtensionMain, so the dead-strip would drop the whole extension.
// Skipped when the extension declares a principal class or a main
// storyboard: the runtime instantiates it and `main` may not exist.
// Otherwise (WidgetKit-style `@main`) it must come from the extension's
// own library, as the builder's stub.c is empty. See Appex.md.
+ (needsMainSymbol ? ["-Xlinker", "-u", "-Xlinker", "_main"] : [])
// Include frameworks that the host app may use
+ ["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks"]
// ...as well as our own
+ ["-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks"]
}
}

/// Whether the extension relies on its own `main` symbol. An extension that names a
/// principal class or a main storyboard is instantiated through the ObjC runtime.
var needsMainSymbol: Bool {
guard let nsExtension = infoPlist["NSExtension"] as? [String: any Sendable] else {
return true
}
return nsExtension["NSExtensionPrincipalClass"] == nil
&& nsExtension["NSExtensionMainStoryboard"] == nil
}

var linkerSettings: String {
let flags = linkerFlags.map { "\"\($0)\"" }.joined(separator: ", ")
switch self.type {
case .application: return """
[
.unsafeFlags([
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
]),
.unsafeFlags([\(flags)]),
]
"""
case .appExtension: """
case .appExtension: return """
[
// Link to Foundation framework which implements the _NSExtensionMain entrypoint
.linkedFramework("Foundation"),
.unsafeFlags([
// Set the entry point to Foundation`_NSExtensionMain
"-Xlinker", "-e", "-Xlinker", "_NSExtensionMain",
// Include frameworks that the host app may use
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks",
// ...as well as our own
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
]),
.unsafeFlags([\(flags)]),
]
"""
}
Expand Down
49 changes: 49 additions & 0 deletions Tests/XToolTests/PackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,52 @@ import Testing

#expect(Packer.cfBundleSupportedPlatforms(for: "unknown-triple") == ["iPhoneOS"])
}

private func product(
_ type: Plan.ProductType,
infoPlist: [String: any Sendable] = [:]
) -> Plan.Product {
Plan.Product(
type: type,
product: "Example",
deploymentTarget: "17.0",
bundleID: "com.example.Example",
infoPlist: infoPlist,
resources: []
)
}

@Test func appExtensionKeepsMainSymbolDespiteForeignEntryPoint() {
let flags = product(.appExtension).linkerFlags
#expect(flags.contains("_NSExtensionMain"))
// The entry point is Foundation's, so nothing references the extension's own `main`
// and the dead-strip would drop the whole extension unless it is forced to stay.
#expect(flags.contains(["-Xlinker", "-u", "-Xlinker", "_main"]))
// ...and the generated Package.swift snippet actually carries the quoted pair.
#expect(product(.appExtension).linkerSettings.contains(#""-u", "-Xlinker", "_main""#))
}

@Test func appExtensionWithoutPrincipalClassKeepsMainSymbol() {
let plist: [String: any Sendable] = ["NSExtension": ["NSExtensionPointIdentifier": "com.apple.widgetkit-extension"]]
let flags = product(.appExtension, infoPlist: plist).linkerFlags
#expect(flags.contains(["-Xlinker", "-u", "-Xlinker", "_main"]))
}

@Test func appExtensionWithPrincipalClassOmitsMainSymbol() {
let plist: [String: any Sendable] = ["NSExtension": ["NSExtensionPrincipalClass": "NotificationService"]]
let flags = product(.appExtension, infoPlist: plist).linkerFlags
#expect(!flags.contains("-u"))
#expect(flags.contains(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"]))
#expect(flags.contains("@executable_path/../../Frameworks"))
}

@Test func appExtensionWithMainStoryboardOmitsMainSymbol() {
let plist: [String: any Sendable] = ["NSExtension": ["NSExtensionMainStoryboard": "MainInterface"]]
let flags = product(.appExtension, infoPlist: plist).linkerFlags
#expect(!flags.contains("-u"))
#expect(flags.contains(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"]))
}

@Test func applicationDoesNotForceMainSymbol() {
#expect(!product(.application).linkerFlags.contains("-u"))
}