From 38209c0c570050d4b11c76f8cbf0d5e16f41dd5f Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:31:19 +0200 Subject: [PATCH 1/3] packer: keep main in app extensions The extension entry point is Foundation's _NSExtensionMain, so nothing references the extension's own main and the linker dead-strips the whole @main (including the example in Appex.md). The binary ends up with no __text, which zsign then refuses to sign. Force the symbol to stay with -u _main. --- Sources/PackLib/Packer.swift | 38 ++++++++++++++++++++---------- Tests/XToolTests/PackerTests.swift | 23 ++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/Sources/PackLib/Packer.swift b/Sources/PackLib/Packer.swift index bd2057b7..f840aafb 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -233,27 +233,39 @@ public struct Packer: Sendable { } extension Plan.Product { + /// The `-Xlinker` and friends passed through `unsafeFlags` for this product. + var linkerFlags: [String] { + switch self.type { + 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. + "-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", + ] + } + } + fileprivate var linkerSettings: String { + let flags = linkerFlags.map { "\"\($0)\"" }.joined(separator: ", ") switch self.type { - case .application: """ + 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..9b6ee23f 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -13,3 +13,26 @@ import Testing #expect(Packer.cfBundleSupportedPlatforms(for: "unknown-triple") == ["iPhoneOS"]) } + +private func product(_ type: Plan.ProductType) -> Plan.Product { + Plan.Product( + type: type, + product: "Example", + deploymentTarget: "17.0", + bundleID: "com.example.Example", + 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"])) +} + +@Test func applicationDoesNotForceMainSymbol() { + #expect(!product(.application).linkerFlags.contains("-u")) +} From 7bacde1b923ecd728773b15422e1d077267ea9bc Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:24:27 +0200 Subject: [PATCH 2/3] packer: document and test the main requirement for extensions The -u _main flag makes an undefined main a link error. The builder's executable target only has an empty stub.c, so main must come from the extension's own library. Note it in Appex.md and next to the flag, and assert the emitted Package.swift snippet carries the quoted pair. --- Documentation/xtool.docc/Appex.md | 2 ++ Sources/PackLib/Packer.swift | 5 ++++- Tests/XToolTests/PackerTests.swift | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Documentation/xtool.docc/Appex.md b/Documentation/xtool.docc/Appex.md index db282bae..ed3c3ed9 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 forces the linker to keep the `main` symbol (`-u _main`), so the extension's library must define one: a `@main` type, as above, or top-level code. A library that only provides an `NSExtensionPrincipalClass` and no `main` fails to link with an undefined `_main` symbol. + 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 f840aafb..5c1acf5d 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -244,6 +244,9 @@ extension Plan.Product { "-Xlinker", "-e", "-Xlinker", "_NSExtensionMain", // Keep `main`: nothing references it when the entry point is // _NSExtensionMain, so the dead-strip would drop the whole extension. + // This makes a `main` symbol mandatory: the builder's executable target + // only has an empty stub.c, so it must come from the extension's own + // library (`@main` or top-level code). See Appex.md. "-Xlinker", "-u", "-Xlinker", "_main", // Include frameworks that the host app may use "-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks", @@ -253,7 +256,7 @@ extension Plan.Product { } } - fileprivate var linkerSettings: String { + var linkerSettings: String { let flags = linkerFlags.map { "\"\($0)\"" }.joined(separator: ", ") switch self.type { case .application: return """ diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index 9b6ee23f..acad19e3 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -31,6 +31,8 @@ private func product(_ type: Plan.ProductType) -> Plan.Product { // 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 applicationDoesNotForceMainSymbol() { From c19dd5e0eaf4d52f6475b8f7803f9b180e0cc665 Mon Sep 17 00:00:00 2001 From: Ugo Mignon <56931733+TartanLeGrand@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:23:15 +0200 Subject: [PATCH 3/3] packer: only force main without a principal class An app extension that declares NSExtensionPrincipalClass or NSExtensionMainStoryboard is instantiated through the ObjC runtime and may have no main, so the unconditional -u _main made notification service and share extensions fail with an undefined symbol. Only add -u _main when NSExtension declares neither key (WidgetKit style @main). The entry point and rpaths are unchanged. --- Documentation/xtool.docc/Appex.md | 2 +- Sources/PackLib/Packer.swift | 28 +++++++++++++++++++--------- Tests/XToolTests/PackerTests.swift | 28 ++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/Documentation/xtool.docc/Appex.md b/Documentation/xtool.docc/Appex.md index ed3c3ed9..6cd7ba5e 100644 --- a/Documentation/xtool.docc/Appex.md +++ b/Documentation/xtool.docc/Appex.md @@ -140,6 +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 forces the linker to keep the `main` symbol (`-u _main`), so the extension's library must define one: a `@main` type, as above, or top-level code. A library that only provides an `NSExtensionPrincipalClass` and no `main` fails to link with an undefined `_main` symbol. +> 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 5c1acf5d..67dbdf07 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -239,21 +239,31 @@ extension Plan.Product { case .application: [ "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", ] - case .appExtension: [ + case .appExtension: // Set the entry point to Foundation`_NSExtensionMain - "-Xlinker", "-e", "-Xlinker", "_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. - // This makes a `main` symbol mandatory: the builder's executable target - // only has an empty stub.c, so it must come from the extension's own - // library (`@main` or top-level code). See Appex.md. - "-Xlinker", "-u", "-Xlinker", "_main", + // 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", + + ["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks"] // ...as well as our own - "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", - ] + + ["-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 { diff --git a/Tests/XToolTests/PackerTests.swift b/Tests/XToolTests/PackerTests.swift index acad19e3..543fdf54 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -14,13 +14,16 @@ import Testing #expect(Packer.cfBundleSupportedPlatforms(for: "unknown-triple") == ["iPhoneOS"]) } -private func product(_ type: Plan.ProductType) -> Plan.Product { +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: infoPlist, resources: [] ) } @@ -35,6 +38,27 @@ private func product(_ type: Plan.ProductType) -> Plan.Product { #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")) }