mirror of
https://github.com/xtool-org/xtool.git
synced 2026-02-04 11:53:30 +01:00
This pull request adds support for Apple's [App Extensions](https://developer.apple.com/app-extensions/). Closes: #45 --------- Co-authored-by: ErrorErrorError <16653389+ErrorErrorError@users.noreply.github.com> Co-authored-by: Kabir Oberai <oberai.kabir@gmail.com>
165 lines
5.8 KiB
Swift
165 lines
5.8 KiB
Swift
#if os(macOS)
|
|
import Foundation
|
|
import PathKit
|
|
import Version
|
|
import ProjectSpec
|
|
import XcodeGenKit
|
|
import XcodeProj
|
|
|
|
public struct XcodePacker {
|
|
public var plan: Plan
|
|
|
|
public init(plan: Plan) {
|
|
self.plan = plan
|
|
}
|
|
|
|
// swiftlint:disable:next function_body_length
|
|
public func createProject() async throws -> URL {
|
|
let xtoolDir: Path = "xtool"
|
|
|
|
let projectDir: Path = xtoolDir + ".xtool-tmp"
|
|
try? xtoolDir.delete()
|
|
try projectDir.mkpath()
|
|
|
|
let fromProjectToRoot = try Path(".").relativePath(from: projectDir)
|
|
|
|
guard let deploymentTarget = Version(tolerant: plan.app.deploymentTarget) else {
|
|
throw StringError("Could not parse deployment target '\(plan.app.deploymentTarget)'")
|
|
}
|
|
|
|
let emptyText = Data("""
|
|
// leave this file empty
|
|
""".utf8)
|
|
|
|
let targets = try plan.allProducts.map { product in
|
|
let productDir = projectDir + product.product
|
|
try productDir.mkpath()
|
|
|
|
let emptyFile = productDir + "empty.c"
|
|
try emptyFile.write(emptyText)
|
|
|
|
let infoPath = productDir + "Info.plist"
|
|
|
|
var plist = product.infoPlist
|
|
let families = (plist.removeValue(forKey: "UIDeviceFamily") as? [Int]) ?? [1, 2]
|
|
plist["CFBundleExecutable"] = product.targetName
|
|
plist["CFBundleName"] = product.targetName
|
|
plist["CFBundleDisplayName"] = product.product
|
|
|
|
let encodedPlist = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
|
|
try infoPath.write(encodedPlist)
|
|
|
|
var buildSettings: [String: Any] = [
|
|
"PRODUCT_BUNDLE_IDENTIFIER": product.bundleID,
|
|
"TARGETED_DEVICE_FAMILY": families.map { "\($0)" }.joined(separator: ","),
|
|
]
|
|
|
|
if product.type == .appExtension {
|
|
buildSettings["APPLICATION_EXTENSION_API_ONLY"] = true
|
|
}
|
|
|
|
if let entitlementsPath = product.entitlementsPath {
|
|
buildSettings["CODE_SIGN_ENTITLEMENTS"] = fromProjectToRoot + Path(entitlementsPath)
|
|
}
|
|
|
|
let additionalDependencies: [Dependency] = if product.type == .application {
|
|
plan.extensions.map {
|
|
Dependency(
|
|
type: .target,
|
|
reference: $0.targetName
|
|
)
|
|
}
|
|
} else {
|
|
[]
|
|
}
|
|
return Target(
|
|
name: product.targetName,
|
|
type: product.type == .application ? .application : .appExtension,
|
|
platform: .iOS,
|
|
deploymentTarget: deploymentTarget,
|
|
settings: Settings(buildSettings: buildSettings),
|
|
sources: [
|
|
TargetSource(
|
|
path: try emptyFile.relativePath(from: projectDir).string,
|
|
buildPhase: .sources
|
|
),
|
|
],
|
|
dependencies: [
|
|
Dependency(
|
|
type: .package(products: [product.product]),
|
|
reference: "RootPackage"
|
|
),
|
|
] + additionalDependencies,
|
|
info: Plist(
|
|
path: try infoPath.relativePath(from: projectDir).string,
|
|
attributes: [:]
|
|
)
|
|
)
|
|
}
|
|
|
|
let project = Project(
|
|
name: plan.app.targetName,
|
|
targets: targets,
|
|
packages: [
|
|
"RootPackage": .local(
|
|
path: fromProjectToRoot.string,
|
|
group: nil,
|
|
excludeFromProject: false
|
|
),
|
|
],
|
|
options: SpecOptions(
|
|
localPackagesGroup: ""
|
|
)
|
|
)
|
|
|
|
// TODO: Handle plan.resources of type .root
|
|
// TODO: Handle plan.iconPath
|
|
|
|
let generator = ProjectGenerator(project: project)
|
|
let xcodeproj = projectDir + "\(plan.app.product).xcodeproj"
|
|
let xcworkspace = xtoolDir + "\(plan.app.product).xcworkspace"
|
|
do {
|
|
let current = Path.current
|
|
Path.current = xcodeproj.parent()
|
|
defer { Path.current = current }
|
|
let xcodeProject = try generator.generateXcodeProject(userName: NSUserName())
|
|
if let packageRef = xcodeProject.pbxproj.fileReferences.first(where: { $0.name == ".." }) {
|
|
for group in xcodeProject.pbxproj.groups {
|
|
group.children.removeAll(where: { $0.uuid == packageRef.uuid })
|
|
}
|
|
xcodeProject.pbxproj.delete(object: packageRef)
|
|
}
|
|
|
|
try xcodeProject.write(path: Path(xcodeproj.lastComponent))
|
|
}
|
|
|
|
do {
|
|
let current = Path.current
|
|
Path.current = xcworkspace.parent()
|
|
defer { Path.current = current }
|
|
|
|
let xcworkspaceDirectory = xcworkspace.parent()
|
|
let fromWorkspaceToSelf = try Path(".").relativePath(from: xcworkspaceDirectory).withName()
|
|
let fromWorkspaceToProject = try xcodeproj.relativePath(from: xcworkspaceDirectory).withName()
|
|
let workspace = XCWorkspace(data: .init(children: [
|
|
.file(.init(location: .container(fromWorkspaceToSelf.string))),
|
|
.file(.init(location: .group(fromWorkspaceToProject.string))),
|
|
]))
|
|
try workspace.write(path: Path(xcworkspace.lastComponent))
|
|
}
|
|
|
|
return xcworkspace.url
|
|
}
|
|
}
|
|
|
|
extension Path {
|
|
fileprivate func withName() -> Path {
|
|
// eg if curr dir is Foo, this converts "." to "../Foo"
|
|
// which includes the name in the path, and therefore
|
|
// in the Xcode navigator
|
|
self.parent() + self.absolute().lastComponent
|
|
}
|
|
}
|
|
|
|
#endif
|