Skip to content

Commit

Permalink
Combine MacOSBundler, IOSBundler, TVOSBundler, and VisionOSBundler in…
Browse files Browse the repository at this point in the history
…to a single DarwinBundler (along with other clean up)

Makes a huge difference to maintainability, they were basically all just slightly changed copies of each other.
Did some basic testing and everything seems to still be working (haven't tested provisioning profile support
yet though, will do once automatic provisioning has been implemented)
  • Loading branch information
stackotter committed Jun 16, 2024
1 parent 893f7a1 commit 90d829b
Show file tree
Hide file tree
Showing 19 changed files with 538 additions and 1,369 deletions.
68 changes: 20 additions & 48 deletions Sources/swift-bundler/Bundler/AppImageBundler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,30 @@ import Foundation

/// The bundler for creating Linux AppImage's.
enum AppImageBundler: Bundler {
/// Bundles the built executable and resources into a Linux AppImage.
///
/// ``build(product:in:buildConfiguration:universal:)`` should usually be called first.
/// - Parameters:
/// - appName: The name to give the bundled app.
/// - packageName: The name of the package.
/// - appConfiguration: The app's configuration.
/// - packageDirectory: The root directory of the package containing the app.
/// - productsDirectory: The directory containing the products from the build step.
/// - outputDirectory: The directory to output the app into.
/// - isXcodeBuild: Whether the build products were created by Xcode or not.
/// - universal: Whether the build products were built as universal binaries or not.
/// - standAlone: If `true`, the app bundle will not depend on any system-wide dependencies
/// being installed (such as gtk).
/// - codesigningIdentity: If not `nil`, the app will be codesigned using the given identity.
/// - provisioningProfile: If not `nil`, this provisioning profile will get embedded in the app.
/// - platformVersion: The platform version that the executable was built for.
/// - targetingSimulator: Does nothing for Linux builds.
/// - Returns: If a failure occurs, it is returned.
typealias Context = Void

static func bundle(
appName: String,
packageName: String,
appConfiguration: AppConfiguration,
packageDirectory: URL,
productsDirectory: URL,
outputDirectory: URL,
isXcodeBuild: Bool,
universal: Bool,
standAlone: Bool,
codesigningIdentity: String?,
codesigningEntitlements: URL?,
provisioningProfile: URL?,
platformVersion: String,
targetingSimulator: Bool
) -> Result<Void, Error> {
log.info("Bundling '\(appName).AppImage'")

let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product)
_ context: BundlerContext,
_ additionalContext: Context
) -> Result<Void, AppImageBundlerError> {
log.info("Bundling '\(context.appName).AppImage'")

let appDir = outputDirectory.appendingPathComponent("\(appName).AppDir")
let appExecutable = appDir.appendingPathComponent("usr/bin/\(appName)")
let executableArtifact = context.productsDirectory.appendingPathComponent(
context.appConfiguration.product)

let appDir = context.outputDirectory.appendingPathComponent("\(context.appName).AppDir")
let appExecutable = appDir.appendingPathComponent("usr/bin/\(context.appName)")
let appIconDirectory = appDir.appendingPathComponent("usr/share/icons/hicolor/1024x1024/apps")

let copyAppIconIfPresent: () -> Result<Void, AppImageBundlerError> = {
let destination = appIconDirectory.appendingPathComponent("\(appName).png")
guard let path = appConfiguration.icon else {
let destination = appIconDirectory.appendingPathComponent("\(context.appName).png")
guard let path = context.appConfiguration.icon else {
// TODO: Synthesize the icon a bit smarter (e.g. maybe an svg would be better to synthesize)
FileManager.default.createFile(atPath: destination.path, contents: nil)
return .success()
}

let icon = packageDirectory.appendingPathComponent(path)
let icon = context.packageDirectory.appendingPathComponent(path)
do {
try FileManager.default.copyItem(at: icon, to: destination)
} catch {
Expand All @@ -63,25 +35,25 @@ enum AppImageBundler: Bundler {
}

let bundleApp = flatten(
{ Self.createAppDirectoryStructure(at: outputDirectory, appName: appName) },
{ Self.createAppDirectoryStructure(at: context.outputDirectory, appName: context.appName) },
{ Self.copyExecutable(at: executableArtifact, to: appExecutable) },
{
Self.createDesktopFile(
at: appDir,
appName: appName,
appConfiguration: appConfiguration
appName: context.appName,
appConfiguration: context.appConfiguration
)
},
copyAppIconIfPresent,
{ Self.createSymlinks(at: appDir, appName: appName) },
{ Self.createSymlinks(at: appDir, appName: context.appName) },
{
log.info("Converting '\(appName).AppDir' to '\(appName).AppImage'")
log.info("Converting '\(context.appName).AppDir' to '\(context.appName).AppImage'")
return AppImageTool.bundle(appDir: appDir)
.mapError { .failedToBundleAppDir($0) }
}
)

return bundleApp().intoAnyError()
return bundleApp()
}

// MARK: Private methods
Expand Down
55 changes: 37 additions & 18 deletions Sources/swift-bundler/Bundler/Bundler.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,53 @@
import Foundation

protocol Bundler {
associatedtype Context
associatedtype Error: LocalizedError

/// Bundles an app from a package's built products directory.
/// - Parameters:
/// - context: The general context passed to all bundlers.
/// - additionalContext: The bundler-specific context for this bundler.
static func bundle(
appName: String,
packageName: String,
appConfiguration: AppConfiguration,
packageDirectory: URL,
productsDirectory: URL,
outputDirectory: URL,
isXcodeBuild: Bool,
universal: Bool,
standAlone: Bool,
codesigningIdentity: String?,
codesigningEntitlements: URL?,
provisioningProfile: URL?,
platformVersion: String,
targetingSimulator: Bool
_ context: BundlerContext,
_ additionalContext: Context
) -> Result<Void, Error>
}

struct BundlerContext {
/// The name to give the bundled app.
var appName: String
/// The name of the package.
var packageName: String
/// The app's configuration.
var appConfiguration: AppConfiguration

/// The root directory of the package containing the app.
var packageDirectory: URL
/// The directory containing the products from the build step.
var productsDirectory: URL
/// The directory to output the app into.
var outputDirectory: URL

/// The platform that the app's product was built for.
var platform: Platform

/// The app's main built executable file.
var executableArtifact: URL {
productsDirectory.appendingPathComponent(appName)
}
}

func getBundler(for platform: Platform) -> any Bundler.Type {
switch platform {
case .macOS:
return MacOSBundler.self
return DarwinBundler.self
case .iOS, .iOSSimulator:
return IOSBundler.self
return DarwinBundler.self
case .tvOS, .tvOSSimulator:
return TVOSBundler.self
return DarwinBundler.self
case .visionOS, .visionOSSimulator:
return VisionOSBundler.self
return DarwinBundler.self
case .linux:
return AppImageBundler.self
}
Expand Down
64 changes: 64 additions & 0 deletions Sources/swift-bundler/Bundler/DarwinAppBundleStructure.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

/// The file/directory structure of a particular app bundle on disk.
struct DarwinAppBundleStructure {
let contentsDirectory: URL
let resourcesDirectory: URL
let librariesDirectory: URL
let executableDirectory: URL
let infoPlistFile: URL
let pkgInfoFile: URL
let provisioningProfileFile: URL
let appIconFile: URL

/// Describes the structure of an app bundle for the specific platform. Doesn't
/// create anything on disk (see ``DarwinAppBundleStructure/createDirectories()``).
init(at bundleDirectory: URL, platform: ApplePlatform) {
let os = platform.os
switch os {
case .macOS:
contentsDirectory = bundleDirectory.appendingPathComponent("Contents")
executableDirectory = contentsDirectory.appendingPathComponent("MacOS")
resourcesDirectory = contentsDirectory.appendingPathComponent("Resources")
case .iOS, .tvOS, .visionOS:
contentsDirectory = bundleDirectory
executableDirectory = contentsDirectory
resourcesDirectory = contentsDirectory
}

librariesDirectory = contentsDirectory.appendingPathComponent("Libraries")

infoPlistFile = contentsDirectory.appendingPathComponent("Info.plist")
pkgInfoFile = contentsDirectory.appendingPathComponent("PkgInfo")
provisioningProfileFile = contentsDirectory.appendingPathComponent(
"embedded.mobileprovision"
)
appIconFile = contentsDirectory.appendingPathComponent("AppIcon.icns")
}

/// Attempts to create all directories within the app bundle. Ignores directories which
/// already exist.
func createDirectories() -> Result<Void, DarwinBundlerError> {
let directories = [
contentsDirectory, resourcesDirectory, librariesDirectory, executableDirectory,
]

for directory in directories {
guard !FileManager.default.itemExists(at: directory, withType: .directory) else {
continue
}
do {
try FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
} catch {
return .failure(
.failedToCreateAppBundleDirectoryStructure(error)
)
}
}

return .success()
}
}
Loading

0 comments on commit 90d829b

Please sign in to comment.