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 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!
43 changes: 29 additions & 14 deletions Sources/PackLib/Packer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)]),
]
"""
}
Expand Down
25 changes: 25 additions & 0 deletions Tests/XToolTests/PackerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}