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/2] 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/2] 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() {