Skip to content

Commit edd6b90

Browse files
Add support for App Extensions (#97)
This pull request adds support for Apple's [App Extensions](https://developer.apple.com/app-extensions/). Closes: #45 --------- Co-authored-by: ErrorErrorError <[email protected]> Co-authored-by: Kabir Oberai <[email protected]>
1 parent bf34edc commit edd6b90

File tree

7 files changed

+444
-196
lines changed

7 files changed

+444
-196
lines changed

Sources/PackLib/PackSchema.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,22 @@ public struct PackSchemaBase: Codable, Sendable {
1818

1919
public var iconPath: String?
2020
public var resources: [String]?
21+
22+
public var extensions: [Extension]?
23+
24+
public struct Extension: Codable, Sendable {
25+
public var product: String
26+
public var bundleID: String?
27+
public var infoPath: String
28+
public var resources: [String]?
29+
public var entitlementsPath: String?
30+
}
2131
}
2232

33+
@dynamicMemberLookup
2334
public struct PackSchema: Sendable {
35+
public typealias Extension = PackSchemaBase.Extension
36+
2437
public enum IDSpecifier: Sendable {
2538
case orgID(String)
2639
case bundleID(String)
@@ -71,4 +84,8 @@ public struct PackSchema: Sendable {
7184
let base = try YAMLDecoder().decode(PackSchemaBase.self, from: data)
7285
try self.init(validating: base)
7386
}
87+
88+
public subscript<Subject>(dynamicMember keyPath: KeyPath<PackSchemaBase, Subject>) -> Subject {
89+
self.base[keyPath: keyPath]
90+
}
7491
}

Sources/PackLib/Packer.swift

Lines changed: 132 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,51 @@ public struct Packer: Sendable {
2020
// swift-tools-version: 6.0
2121
import PackageDescription
2222
let package = Package(
23-
name: "\(plan.product)-Builder",
23+
name: "\(plan.app.product)-Builder",
2424
platforms: [
25-
.iOS("\(plan.deploymentTarget)"),
25+
.iOS("\(plan.app.deploymentTarget)"),
2626
],
2727
dependencies: [
2828
.package(name: "RootPackage", path: "../.."),
2929
],
3030
targets: [
31-
.executableTarget(
32-
name: "\(plan.product)-App",
33-
dependencies: [
34-
.product(name: "\(plan.product)", package: "RootPackage"),
35-
]
36-
),
31+
\(
32+
plan.allProducts.map {
33+
"""
34+
.executableTarget(
35+
name: "\($0.targetName)",
36+
dependencies: [
37+
.product(name: "\($0.product)", package: "RootPackage"),
38+
],
39+
linkerSettings: \($0.linkerSettings)
40+
)
41+
"""
42+
}
43+
.joined(separator: ",\n")
44+
)
3745
]
3846
)\n
3947
"""
4048
try Data(contents.utf8).write(to: packageSwift)
41-
let sources = packageDir.appendingPathComponent("Sources")
42-
try? FileManager.default.createDirectory(at: sources, withIntermediateDirectories: true)
43-
try Data().write(to: sources.appendingPathComponent("stub.c"))
49+
50+
for product in plan.allProducts {
51+
let sources: URL = packageDir.appendingPathComponent("Sources/\(product.targetName)", isDirectory: true)
52+
try FileManager.default.createDirectory(at: sources, withIntermediateDirectories: true)
53+
try Data().write(to: sources.appendingPathComponent("stub.c", isDirectory: false))
54+
}
4455

4556
let builder = try await buildSettings.swiftPMInvocation(
4657
forTool: "build",
4758
arguments: [
4859
"--package-path", packageDir.path,
4960
"--scratch-path", ".build",
50-
"--product", "\(plan.product)-App",
5161
// resolving can cause SwiftPM to overwrite the root package deps
5262
// with just the deps needed for the builder package (which is to
5363
// say, any "dev dependencies" of the root package may be removed.)
5464
// fortunately we've already resolved the root package by this point
5565
// in order to dump the plan, so we can skip resolution here to skirt
5666
// the issue.
5767
"--disable-automatic-resolution",
58-
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
5968
]
6069
)
6170
builder.standardOutput = FileHandle.standardError
@@ -65,15 +74,49 @@ public struct Packer: Sendable {
6574
public func pack() async throws -> URL {
6675
try await build()
6776

68-
let output = try TemporaryDirectory(name: "\(plan.product).app")
77+
let output = try TemporaryDirectory(name: "\(plan.app.product).app")
78+
79+
let outputURL = output.url
6980

7081
let binDir = URL(
7182
fileURLWithPath: ".build/\(buildSettings.triple)/\(buildSettings.configuration.rawValue)",
7283
isDirectory: true
7384
)
7485

75-
let outputURL = output.url
86+
try await withThrowingTaskGroup(of: Void.self) { group in
87+
for product in plan.allProducts {
88+
try pack(
89+
product: product,
90+
binDir: binDir,
91+
outputURL: product.directory(inApp: outputURL),
92+
&group
93+
)
94+
}
7695

96+
while !group.isEmpty {
97+
do {
98+
try await group.next()
99+
} catch is CancellationError {
100+
// continue
101+
} catch {
102+
group.cancelAll()
103+
throw error
104+
}
105+
}
106+
}
107+
108+
let dest = URL(fileURLWithPath: "xtool").appendingPathComponent(outputURL.lastPathComponent)
109+
try? FileManager.default.removeItem(at: dest)
110+
try output.persist(at: dest)
111+
return dest
112+
}
113+
114+
@Sendable private func pack(
115+
product: Plan.Product,
116+
binDir: URL,
117+
outputURL: URL,
118+
_ group: inout ThrowingTaskGroup<Void, Error>
119+
) throws {
77120
@Sendable func packFileToRoot(srcName: String) async throws {
78121
let srcURL = URL(fileURLWithPath: srcName)
79122
let destURL = outputURL.appendingPathComponent(srcURL.lastPathComponent)
@@ -91,70 +134,89 @@ public struct Packer: Sendable {
91134
try Task.checkCancellation()
92135
}
93136

94-
try await withThrowingTaskGroup(of: Void.self) { group in
95-
for command in plan.resources {
96-
group.addTask {
97-
switch command {
98-
case .bundle(let package, let target):
99-
try await packFile(srcName: "\(package)_\(target).bundle")
100-
case .binaryTarget(let name):
101-
let src = URL(fileURLWithPath: "\(name).framework/\(name)", relativeTo: binDir)
102-
let magic = Data("!<arch>\n".utf8)
103-
let thinMagic = Data("!<thin>\n".utf8)
104-
let bytes = try FileHandle(forReadingFrom: src).read(upToCount: magic.count)
105-
// if the magic matches one of these it's a static archive; don't embed it.
106-
// https://github.com/apple/llvm-project/blob/e716ff14c46490d2da6b240806c04e2beef01f40/llvm/include/llvm/Object/Archive.h#L33
107-
// swiftlint:disable:previous line_length
108-
if bytes != magic && bytes != thinMagic {
109-
try await packFile(srcName: "\(name).framework", dstName: "Frameworks/\(name).framework", sign: true)
110-
}
111-
case .library(let name):
112-
try await packFile(srcName: "lib\(name).dylib", dstName: "Frameworks/lib\(name).dylib", sign: true)
113-
case .root(let source):
114-
try await packFileToRoot(srcName: source)
137+
// Ensure output directory is available
138+
try? FileManager.default.createDirectory(at: outputURL, withIntermediateDirectories: true)
139+
140+
for command in product.resources {
141+
group.addTask {
142+
switch command {
143+
case .bundle(let package, let target):
144+
try await packFile(srcName: "\(package)_\(target).bundle")
145+
case .binaryTarget(let name):
146+
let src = URL(fileURLWithPath: "\(name).framework/\(name)", relativeTo: binDir)
147+
let magic = Data("!<arch>\n".utf8)
148+
let thinMagic = Data("!<thin>\n".utf8)
149+
let bytes = try FileHandle(forReadingFrom: src).read(upToCount: magic.count)
150+
// if the magic matches one of these it's a static archive; don't embed it.
151+
// https://github.com/apple/llvm-project/blob/e716ff14c46490d2da6b240806c04e2beef01f40/llvm/include/llvm/Object/Archive.h#L33
152+
// swiftlint:disable:previous line_length
153+
if bytes != magic && bytes != thinMagic {
154+
try await packFile(srcName: "\(name).framework", dstName: "Frameworks/\(name).framework", sign: true)
115155
}
156+
case .library(let name):
157+
try await packFile(srcName: "lib\(name).dylib", dstName: "Frameworks/lib\(name).dylib", sign: true)
158+
case .root(let source):
159+
try await packFileToRoot(srcName: source)
116160
}
117161
}
118-
if let iconPath = plan.iconPath {
119-
group.addTask {
120-
try await packFileToRoot(srcName: iconPath)
121-
}
122-
}
162+
}
163+
if let iconPath = product.iconPath {
123164
group.addTask {
124-
try await packFile(srcName: "\(plan.product)-App", dstName: plan.product)
165+
try await packFileToRoot(srcName: iconPath)
125166
}
126-
group.addTask {
127-
var info = plan.infoPlist
128-
129-
if let iconPath = plan.iconPath {
130-
let iconName = URL(fileURLWithPath: iconPath).deletingPathExtension().lastPathComponent
131-
info["CFBundleIconFile"] = iconName
132-
}
167+
}
168+
group.addTask {
169+
try await packFile(srcName: product.targetName, dstName: product.product)
170+
}
171+
group.addTask {
172+
var info = product.infoPlist
133173

134-
let infoPath = outputURL.appendingPathComponent("Info.plist")
135-
let encodedPlist = try PropertyListSerialization.data(
136-
fromPropertyList: info,
137-
format: .xml,
138-
options: 0
139-
)
140-
try encodedPlist.write(to: infoPath)
174+
if product.type == .application {
175+
info["UIRequiredDeviceCapabilities"] = ["arm64"]
176+
info["LSRequiresIPhoneOS"] = true
177+
info["CFBundleSupportedPlatforms"] = ["iPhoneOS"]
141178
}
142-
while !group.isEmpty {
143-
do {
144-
try await group.next()
145-
} catch is CancellationError {
146-
// continue
147-
} catch {
148-
group.cancelAll()
149-
throw error
150-
}
179+
180+
if let iconPath = product.iconPath {
181+
let iconName = URL(fileURLWithPath: iconPath).deletingPathExtension().lastPathComponent
182+
info["CFBundleIconFile"] = iconName
151183
}
184+
185+
let infoPath = outputURL.appendingPathComponent("Info.plist")
186+
let encodedPlist = try PropertyListSerialization.data(
187+
fromPropertyList: info,
188+
format: .xml,
189+
options: 0
190+
)
191+
try encodedPlist.write(to: infoPath)
152192
}
193+
}
194+
}
153195

154-
let dest = URL(fileURLWithPath: "xtool")
155-
.appendingPathComponent(output.url.lastPathComponent)
156-
try? FileManager.default.removeItem(at: dest)
157-
try output.persist(at: dest)
158-
return dest
196+
extension Plan.Product {
197+
fileprivate var linkerSettings: String {
198+
switch self.type {
199+
case .application: """
200+
[
201+
.unsafeFlags([
202+
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
203+
]),
204+
]
205+
"""
206+
case .appExtension: """
207+
[
208+
// Link to Foundation framework which implements the _NSExtensionMain entrypoint
209+
.linkedFramework("Foundation"),
210+
.unsafeFlags([
211+
// Set the entry point to Foundation`_NSExtensionMain
212+
"-Xlinker", "-e", "-Xlinker", "_NSExtensionMain",
213+
// Include frameworks that the host app may use
214+
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../Frameworks",
215+
// ...as well as our own
216+
"-Xlinker", "-rpath", "-Xlinker", "@executable_path/Frameworks",
217+
]),
218+
]
219+
"""
220+
}
159221
}
160222
}

0 commit comments

Comments
 (0)