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 bd2057b7..5c1acf5d 100644 --- a/Sources/PackLib/Packer.swift +++ b/Sources/PackLib/Packer.swift @@ -233,27 +233,42 @@ 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. + // 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", + // ...as well as our own + "-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks", + ] + } + } + + 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..acad19e3 100644 --- a/Tests/XToolTests/PackerTests.swift +++ b/Tests/XToolTests/PackerTests.swift @@ -13,3 +13,28 @@ 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"])) + // ...and the generated Package.swift snippet actually carries the quoted pair. + #expect(product(.appExtension).linkerSettings.contains(#""-u", "-Xlinker", "_main""#)) +} + +@Test func applicationDoesNotForceMainSymbol() { + #expect(!product(.application).linkerFlags.contains("-u")) +}