diff --git a/Documentation/xtool.docc/Appex.md b/Documentation/xtool.docc/Appex.md index db282bae..6cd7ba5e 100644 --- a/Documentation/xtool.docc/Appex.md +++ b/Documentation/xtool.docc/Appex.md @@ -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! diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index bd2057b7..67dbdf07 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -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)]), ] """ } diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index 5aabda83..543fdf54 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -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")) +}