From 90d829b58723f62c06b737b0e92249c53b4d9653 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 16 Jun 2024 18:16:23 +1000 Subject: [PATCH] Combine MacOSBundler, IOSBundler, TVOSBundler, and VisionOSBundler into 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) --- .../Bundler/AppImageBundler.swift | 68 ++--- Sources/swift-bundler/Bundler/Bundler.swift | 55 ++-- .../Bundler/DarwinAppBundleStructure.swift | 64 ++++ .../swift-bundler/Bundler/DarwinBundler.swift | 250 ++++++++++++++++ ...erError.swift => DarwinBundlerError.swift} | 16 +- .../swift-bundler/Bundler/IOSBundler.swift | 274 ----------------- .../Bundler/IOSBundlerError.swift | 62 ---- .../Bundler/IconSetCreator.swift | 11 +- .../swift-bundler/Bundler/MacOSBundler.swift | 252 ---------------- .../Bundler/SwiftPackageManager/AppleOS.swift | 9 + .../SwiftPackageManager/ApplePlatform.swift | 43 +++ .../Bundler/SwiftPackageManager/OS.swift | 10 + .../SwiftPackageManager/Platform.swift | 42 ++- .../swift-bundler/Bundler/TVOSBundler.swift | 280 ------------------ .../Bundler/TVOSBundlerError.swift | 62 ---- .../Bundler/VisionOSBundler.swift | 276 ----------------- .../Bundler/VisionOSBundlerError.swift | 62 ---- .../Commands/BundleCommand.swift | 52 ++-- .../Configuration/PackageConfiguration.swift | 19 +- 19 files changed, 538 insertions(+), 1369 deletions(-) create mode 100644 Sources/swift-bundler/Bundler/DarwinAppBundleStructure.swift create mode 100644 Sources/swift-bundler/Bundler/DarwinBundler.swift rename Sources/swift-bundler/Bundler/{MacOSBundlerError.swift => DarwinBundlerError.swift} (80%) delete mode 100644 Sources/swift-bundler/Bundler/IOSBundler.swift delete mode 100644 Sources/swift-bundler/Bundler/IOSBundlerError.swift delete mode 100644 Sources/swift-bundler/Bundler/MacOSBundler.swift create mode 100644 Sources/swift-bundler/Bundler/SwiftPackageManager/AppleOS.swift create mode 100644 Sources/swift-bundler/Bundler/SwiftPackageManager/ApplePlatform.swift create mode 100644 Sources/swift-bundler/Bundler/SwiftPackageManager/OS.swift delete mode 100644 Sources/swift-bundler/Bundler/TVOSBundler.swift delete mode 100644 Sources/swift-bundler/Bundler/TVOSBundlerError.swift delete mode 100644 Sources/swift-bundler/Bundler/VisionOSBundler.swift delete mode 100644 Sources/swift-bundler/Bundler/VisionOSBundlerError.swift diff --git a/Sources/swift-bundler/Bundler/AppImageBundler.swift b/Sources/swift-bundler/Bundler/AppImageBundler.swift index 0ed2693..2e4a505 100644 --- a/Sources/swift-bundler/Bundler/AppImageBundler.swift +++ b/Sources/swift-bundler/Bundler/AppImageBundler.swift @@ -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 { - log.info("Bundling '\(appName).AppImage'") - - let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product) + _ context: BundlerContext, + _ additionalContext: Context + ) -> Result { + 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 = { - 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 { @@ -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 diff --git a/Sources/swift-bundler/Bundler/Bundler.swift b/Sources/swift-bundler/Bundler/Bundler.swift index 923958b..9dbe532 100644 --- a/Sources/swift-bundler/Bundler/Bundler.swift +++ b/Sources/swift-bundler/Bundler/Bundler.swift @@ -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 } +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 } diff --git a/Sources/swift-bundler/Bundler/DarwinAppBundleStructure.swift b/Sources/swift-bundler/Bundler/DarwinAppBundleStructure.swift new file mode 100644 index 0000000..795966f --- /dev/null +++ b/Sources/swift-bundler/Bundler/DarwinAppBundleStructure.swift @@ -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 { + 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() + } +} diff --git a/Sources/swift-bundler/Bundler/DarwinBundler.swift b/Sources/swift-bundler/Bundler/DarwinBundler.swift new file mode 100644 index 0000000..00972c3 --- /dev/null +++ b/Sources/swift-bundler/Bundler/DarwinBundler.swift @@ -0,0 +1,250 @@ +import Foundation + +/// The bundler for creating macOS apps. +enum DarwinBundler: Bundler { + struct Context { + /// Whether the build products were created by Xcode or not. + var isXcodeBuild: Bool + /// Whether the build products were built as universal binaries or not. + var universal: Bool + /// If `true`, the app bundle will not depend on any system-wide dependencies + /// stalled (such as gtk). + var standAlone: Bool + /// The platform that the app should be bundled for. + var platform: ApplePlatform + /// The platform version that the executable was built for. + var platformVersion: String + /// The code signing context if code signing has been requested. + var codeSigningContext: CodeSigningContext? + + struct CodeSigningContext { + /// The identity to sign the app with. + var identity: String + /// A file containing entitlements to give the app if codesigning. + var entitlements: URL? + /// If not `nil`, this provisioning profile will get embedded in the app. + var provisioningProfile: URL? + } + } + + static func bundle( + _ context: BundlerContext, + _ additionalContext: Context + ) -> Result { + log.info("Bundling '\(context.appName).app'") + + let appBundle = context.outputDirectory.appendingPathComponent("\(context.appName).app") + let bundleStructure = DarwinAppBundleStructure( + at: appBundle, + platform: additionalContext.platform + ) + let appExecutable = bundleStructure.executableDirectory.appendingPathComponent(context.appName) + + let removeExistingAppBundle: () -> Result = { + if FileManager.default.itemExists(at: appBundle, withType: .directory) { + do { + try FileManager.default.removeItem(at: appBundle) + } catch { + return .failure(.failedToRemoveExistingAppBundle(bundle: appBundle, error)) + } + } + return .success() + } + + let createAppIconIfPresent: () -> Result = { + if let path = context.appConfiguration.icon { + let icon = context.packageDirectory.appendingPathComponent(path) + return Self.compileAppIcon(at: icon, to: bundleStructure.appIconFile) + } + return .success() + } + + let copyResourcesBundles: () -> Result = { + ResourceBundler.copyResources( + from: context.productsDirectory, + to: bundleStructure.resourcesDirectory, + fixBundles: !additionalContext.isXcodeBuild && !additionalContext.universal, + platform: context.platform, + platformVersion: additionalContext.platformVersion, + packageName: context.packageName, + productName: context.appConfiguration.product + ).mapError { error in + .failedToCopyResourceBundles(error) + } + } + + let copyDynamicLibraries: () -> Result = { + DynamicLibraryBundler.copyDynamicLibraries( + dependedOnBy: appExecutable, + to: bundleStructure.librariesDirectory, + productsDirectory: context.productsDirectory, + isXcodeBuild: additionalContext.isXcodeBuild, + universal: additionalContext.universal, + makeStandAlone: additionalContext.standAlone + ).mapError { error in + .failedToCopyDynamicLibraries(error) + } + } + + let embedProfile: () -> Result = { + guard + let provisioningProfile = additionalContext.codeSigningContext?.provisioningProfile + else { + return .success() + } + return Self.embedProvisioningProfile(provisioningProfile, in: appBundle) + } + + let sign: () -> Result = { + if let codeSigningContext = additionalContext.codeSigningContext { + return CodeSigner.signAppBundle( + bundle: appBundle, + identityId: codeSigningContext.identity, + entitlements: codeSigningContext.entitlements + ).mapError { error in + return .failedToCodesign(error) + } + } else { + return .success() + } + } + + let bundleApp = flatten( + removeExistingAppBundle, + bundleStructure.createDirectories, + { Self.copyExecutable(at: context.executableArtifact, to: appExecutable) }, + { Self.createPkgInfoFile(at: bundleStructure.pkgInfoFile) }, + { + Self.createInfoPlistFile( + at: bundleStructure.infoPlistFile, + appName: context.appName, + appConfiguration: context.appConfiguration, + platform: additionalContext.platform, + platformVersion: additionalContext.platformVersion + ) + }, + createAppIconIfPresent, + copyResourcesBundles, + copyDynamicLibraries, + embedProfile, + sign + ) + + return bundleApp() + } + + // MARK: Private methods + + /// Copies the built executable into the app bundle. + /// - Parameters: + /// - source: The location of the built executable. + /// - destination: The target location of the built executable (the file not the directory). + /// - Returns: If an error occurs, a failure is returned. + private static func copyExecutable( + at source: URL, to destination: URL + ) -> Result { + log.info("Copying executable") + do { + try FileManager.default.copyItem(at: source, to: destination) + return .success() + } catch { + return .failure(.failedToCopyExecutable(source: source, destination: destination, error)) + } + } + + /// Create's a `PkgInfo` file. + /// - Parameters: + /// - pkgInfoFile: the location of the output `PkgInfo` file (needn't exist yet). + /// - Returns: If an error occurs, a failure is returned. + private static func createPkgInfoFile(at pkgInfoFile: URL) -> Result { + log.info("Creating 'PkgInfo'") + do { + var pkgInfoBytes: [UInt8] = [0x41, 0x50, 0x50, 0x4c, 0x3f, 0x3f, 0x3f, 0x3f] + let pkgInfoData = Data(bytes: &pkgInfoBytes, count: pkgInfoBytes.count) + try pkgInfoData.write(to: pkgInfoFile) + return .success() + } catch { + return .failure(.failedToCreatePkgInfo(file: pkgInfoFile, error)) + } + } + + /// Creates an app's `Info.plist` file. + /// - Parameters: + /// - infoPlistFile: The output `Info.plist` file (needn't exist yet). + /// - appName: The app's name. + /// - appConfiguration: The app's configuration. + /// - macOSVersion: The macOS version to target. + /// - Returns: If an error occurs, a failure is returned. + private static func createInfoPlistFile( + at infoPlistFile: URL, + appName: String, + appConfiguration: AppConfiguration, + platform: ApplePlatform, + platformVersion: String + ) -> Result { + log.info("Creating 'Info.plist'") + return PlistCreator.createAppInfoPlist( + at: infoPlistFile, + appName: appName, + configuration: appConfiguration, + platform: platform.platform, + platformVersion: platformVersion + ).mapError { error in + .failedToCreateInfoPlist(error) + } + } + + /// If given an `icns`, the `icns` gets copied to the output file. If given a `png`, an `icns` is created from the `png`. + /// + /// The files are not validated any further than checking their file extensions. + /// - Parameters: + /// - inputIconFile: The app's icon. Should be either an `icns` file or a 1024x1024 `png` with an alpha channel. + /// - outputIconFile: The `icns` file to output to. + /// - Returns: If the png exists and there is an error while converting it to `icns`, a failure is returned. + /// If the file is neither an `icns` or a `png`, a failure is also returned. + private static func compileAppIcon( + at inputIconFile: URL, + to outputIconFile: URL + ) -> Result { + // Copy `AppIcon.icns` if present + if inputIconFile.pathExtension == "icns" { + log.info("Copying '\(inputIconFile.lastPathComponent)'") + do { + try FileManager.default.copyItem(at: inputIconFile, to: outputIconFile) + return .success() + } catch { + return .failure( + .failedToCopyICNS(source: inputIconFile, destination: outputIconFile, error) + ) + } + } else if inputIconFile.pathExtension == "png" { + log.info( + "Creating '\(outputIconFile.lastPathComponent)' from '\(inputIconFile.lastPathComponent)'" + ) + return IconSetCreator.createIcns(from: inputIconFile, outputFile: outputIconFile) + .mapError { error in + .failedToCreateIcon(error) + } + } + + return .failure(.invalidAppIconFile(inputIconFile)) + } + + private static func embedProvisioningProfile( + _ provisioningProfile: URL, + in bundle: URL + ) -> Result { + log.info("Embedding provisioning profile") + + do { + try FileManager.default.copyItem( + at: provisioningProfile, + to: bundle.appendingPathComponent("embedded.mobileprovision") + ) + } catch { + return .failure(.failedToCopyProvisioningProfile(error)) + } + + return .success() + } +} diff --git a/Sources/swift-bundler/Bundler/MacOSBundlerError.swift b/Sources/swift-bundler/Bundler/DarwinBundlerError.swift similarity index 80% rename from Sources/swift-bundler/Bundler/MacOSBundlerError.swift rename to Sources/swift-bundler/Bundler/DarwinBundlerError.swift index e646541..6dc71e6 100644 --- a/Sources/swift-bundler/Bundler/MacOSBundlerError.swift +++ b/Sources/swift-bundler/Bundler/DarwinBundlerError.swift @@ -1,9 +1,10 @@ import Foundation -/// An error returned by ``MacOSBundler``. -enum MacOSBundlerError: LocalizedError { +/// An error returned by ``DarwinBundler``. +enum DarwinBundlerError: LocalizedError { case failedToBuild(product: String, SwiftPackageManagerError) - case failedToCreateAppBundleDirectoryStructure(bundleDirectory: URL, Error) + case failedToRemoveExistingAppBundle(bundle: URL, Error) + case failedToCreateAppBundleDirectoryStructure(Error) case failedToCreatePkgInfo(file: URL, Error) case failedToCreateInfoPlist(PlistCreatorError) case failedToCopyExecutable(source: URL, destination: URL, Error) @@ -16,13 +17,16 @@ enum MacOSBundlerError: LocalizedError { case failedToCodesign(CodeSignerError) case failedToLoadManifest(SwiftPackageManagerError) case failedToGetMinimumMacOSVersion(manifest: URL) + case failedToCopyProvisioningProfile(Error) var errorDescription: String? { switch self { case .failedToBuild(let product, let swiftPackageManagerError): return "Failed to build '\(product)': \(swiftPackageManagerError.localizedDescription)'" - case .failedToCreateAppBundleDirectoryStructure(let bundleDirectory, _): - return "Failed to create app bundle directory structure at '\(bundleDirectory)'" + case .failedToRemoveExistingAppBundle(let bundle, _): + return "Failed to remove existing app bundle at '\(bundle.relativePath)'" + case .failedToCreateAppBundleDirectoryStructure(let error): + return "Failed to create app bundle directory structure: \(error)" case .failedToCreatePkgInfo(let file, _): return "Failed to create 'PkgInfo' file at '\(file)'" case .failedToCreateInfoPlist(let plistCreatorError): @@ -51,6 +55,8 @@ enum MacOSBundlerError: LocalizedError { case .failedToGetMinimumMacOSVersion(let manifest): return "To build for macOS, please specify a macOS deployment version in the platforms field of '\(manifest.relativePath)'" + case .failedToCopyProvisioningProfile: + return "Failed to copy provisioning profile to output bundle" } } } diff --git a/Sources/swift-bundler/Bundler/IOSBundler.swift b/Sources/swift-bundler/Bundler/IOSBundler.swift deleted file mode 100644 index 631feed..0000000 --- a/Sources/swift-bundler/Bundler/IOSBundler.swift +++ /dev/null @@ -1,274 +0,0 @@ -import Foundation - -/// The bundler for creating iOS apps. -enum IOSBundler: Bundler { - /// Bundles the built executable and resources into an iOS app. - /// - /// ``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: Does nothing for iOS. - /// - universal: Does nothing for iOS. - /// - 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: If `true`, the app should be bundled for running on a simulator. - /// - Returns: If a failure occurs, it is returned. - 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 { - log.info("Bundling '\(appName).app'") - - let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product) - - let appBundle = outputDirectory.appendingPathComponent("\(appName).app") - let appExecutable = appBundle.appendingPathComponent(appName) - let appDynamicLibrariesDirectory = appBundle.appendingPathComponent("Libraries") - - // let createAppIconIfPresent: () -> Result = { - // if let path = appConfiguration.icon { - // let icon = packageDirectory.appendingPathComponent(path) - // return Self.createAppIcon(icon: icon, outputDirectory: appAssets) - // } - // return .success() - // } - - let copyResourcesBundles: () -> Result = { - ResourceBundler.copyResources( - from: productsDirectory, - to: appBundle, - fixBundles: !isXcodeBuild && !universal, - platform: targetingSimulator ? .iOSSimulator : .iOS, - platformVersion: platformVersion, - packageName: packageName, - productName: appConfiguration.product - ).mapError { error in - .failedToCopyResourceBundles(error) - } - } - - let copyDynamicLibraries: () -> Result = { - DynamicLibraryBundler.copyDynamicLibraries( - dependedOnBy: appExecutable, - to: appDynamicLibrariesDirectory, - productsDirectory: productsDirectory, - isXcodeBuild: false, - universal: false, - makeStandAlone: false - ).mapError { error in - .failedToCopyDynamicLibraries(error) - } - } - - let embedProfile: () -> Result = { - if let provisioningProfile = provisioningProfile { - return Self.embedProvisioningProfile(provisioningProfile, in: appBundle) - } else { - return .success() - } - } - - let codesign: () -> Result = { - if let identity = codesigningIdentity { - return CodeSigner.signWithGeneratedEntitlements( - bundle: appBundle, - identityId: identity, - bundleIdentifier: appConfiguration.identifier - ).mapError { error in - return .failedToCodesign(error) - } - } else { - return .success() - } - } - - // TODO: Why is app icon creation disabled? - let bundleApp = flatten( - { Self.createAppDirectoryStructure(at: outputDirectory, appName: appName) }, - { Self.copyExecutable(at: executableArtifact, to: appExecutable) }, - { - Self.createMetadataFiles( - at: appBundle, - appName: appName, - appConfiguration: appConfiguration, - iOSVersion: platformVersion, - targetingSimulator: targetingSimulator - ) - }, - // { createAppIconIfPresent() }, - { copyResourcesBundles() }, - { copyDynamicLibraries() }, - { embedProfile() }, - { codesign() } - ) - - return bundleApp().mapError { (error: IOSBundlerError) -> Error in - return error - } - } - - // MARK: Private methods - - /// Creates the directory structure for an app. - /// - /// Creates the following structure: - /// - /// - `AppName.app` - /// - `Libraries` - /// - /// If the app directory already exists, it is deleted before continuing. - /// - /// - Parameters: - /// - outputDirectory: The directory to output the app to. - /// - appName: The name of the app. - /// - Returns: A failure if directory creation fails. - private static func createAppDirectoryStructure( - at outputDirectory: URL, - appName: String - ) -> Result { - log.info("Creating '\(appName).app'") - let fileManager = FileManager.default - - let appBundleDirectory = outputDirectory.appendingPathComponent("\(appName).app") - let appDynamicLibrariesDirectory = appBundleDirectory.appendingPathComponent("Libraries") - - do { - if fileManager.itemExists(at: appBundleDirectory, withType: .directory) { - try fileManager.removeItem(at: appBundleDirectory) - } - try fileManager.createDirectory(at: appBundleDirectory) - try fileManager.createDirectory(at: appDynamicLibrariesDirectory) - return .success() - } catch { - return .failure( - .failedToCreateAppBundleDirectoryStructure(bundleDirectory: appBundleDirectory, error)) - } - } - - /// Copies the built executable into the app bundle. - /// - Parameters: - /// - source: The location of the built executable. - /// - destination: The target location of the built executable (the file not the directory). - /// - Returns: If an error occus, a failure is returned. - private static func copyExecutable( - at source: URL, - to destination: URL - ) -> Result { - log.info("Copying executable") - do { - try FileManager.default.copyItem(at: source, to: destination) - return .success() - } catch { - return .failure(.failedToCopyExecutable(source: source, destination: destination, error)) - } - } - - /// Creates an app's `PkgInfo` and `Info.plist` files. - /// - Parameters: - /// - outputDirectory: Should be the app's `Contents` directory. - /// - appName: The app's name. - /// - appConfiguration: The app's configuration. - /// - iOSVersion: The iOS version to target. - /// - targetingSimulator: If `true`, the files will be created for running in a simulator. - /// - Returns: If an error occurs, a failure is returned. - private static func createMetadataFiles( - at outputDirectory: URL, - appName: String, - appConfiguration: AppConfiguration, - iOSVersion: String, - targetingSimulator: Bool - ) -> Result { - log.info("Creating 'PkgInfo'") - let pkgInfoFile = outputDirectory.appendingPathComponent("PkgInfo") - do { - var pkgInfoBytes: [UInt8] = [0x41, 0x50, 0x50, 0x4c, 0x3f, 0x3f, 0x3f, 0x3f] - let pkgInfoData = Data(bytes: &pkgInfoBytes, count: pkgInfoBytes.count) - try pkgInfoData.write(to: pkgInfoFile) - } catch { - return .failure(.failedToCreatePkgInfo(file: pkgInfoFile, error)) - } - - log.info("Creating 'Info.plist'") - let infoPlistFile = outputDirectory.appendingPathComponent("Info.plist") - return PlistCreator.createAppInfoPlist( - at: infoPlistFile, - appName: appName, - configuration: appConfiguration, - platform: targetingSimulator ? .iOSSimulator : .iOS, - platformVersion: iOSVersion - ).mapError { error in - .failedToCreateInfoPlist(error) - } - } - - /// If given an `icns`, the `icns` gets copied to the output directory. If given a `png`, an `AppIcon.icns` is created from the `png`. - /// - /// The files are not validated any further than checking their file extensions. - /// - Parameters: - /// - icon: The app's icon. Should be either an `icns` file or a 1024x1024 `png` with an alpha channel. - /// - outputDirectory: Should be the app's `Resources` directory. - /// - Returns: If the png exists and there is an error while converting it to `icns`, a failure is returned. - /// If the file is neither an `icns` or a `png`, a failure is also returned. - private static func createAppIcon( - icon: URL, - outputDirectory: URL - ) -> Result { - // Copy `AppIcon.icns` if present - if icon.pathExtension == "icns" { - log.info("Copying '\(icon.lastPathComponent)'") - let destination = outputDirectory.appendingPathComponent("AppIcon.icns") - do { - try FileManager.default.copyItem(at: icon, to: destination) - return .success() - } catch { - return .failure(.failedToCopyICNS(source: icon, destination: destination, error)) - } - } else if icon.pathExtension == "png" { - log.info("Creating 'AppIcon.icns' from '\(icon.lastPathComponent)'") - return IconSetCreator.createIcns(from: icon, outputDirectory: outputDirectory) - .mapError { error in - .failedToCreateIcon(error) - } - } - - return .failure(.invalidAppIconFile(icon)) - } - - private static func embedProvisioningProfile( - _ provisioningProfile: URL, - in bundle: URL - ) -> Result { - log.info("Embedding provisioning profile") - - do { - try FileManager.default.copyItem( - at: provisioningProfile, - to: bundle.appendingPathComponent("embedded.mobileprovision") - ) - } catch { - return .failure(.failedToCopyProvisioningProfile(error)) - } - - return .success() - } -} diff --git a/Sources/swift-bundler/Bundler/IOSBundlerError.swift b/Sources/swift-bundler/Bundler/IOSBundlerError.swift deleted file mode 100644 index cac3ca9..0000000 --- a/Sources/swift-bundler/Bundler/IOSBundlerError.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -/// An error returned by ``IOSBundler``. -enum IOSBundlerError: LocalizedError { - case failedToBuild(product: String, SwiftPackageManagerError) - case failedToCreateAppBundleDirectoryStructure(bundleDirectory: URL, Error) - case failedToCreatePkgInfo(file: URL, Error) - case failedToCreateInfoPlist(PlistCreatorError) - case failedToCopyExecutable(source: URL, destination: URL, Error) - case failedToCreateIcon(IconSetCreatorError) - case failedToCopyICNS(source: URL, destination: URL, Error) - case failedToCopyResourceBundles(ResourceBundlerError) - case failedToCopyDynamicLibraries(DynamicLibraryBundlerError) - case failedToRunExecutable(ProcessError) - case invalidAppIconFile(URL) - case failedToCopyProvisioningProfile(Error) - case failedToCodesign(CodeSignerError) - case mustSpecifyBundleIdentifier - case failedToLoadManifest(SwiftPackageManagerError) - case failedToGetMinimumIOSVersion(manifest: URL) - - var errorDescription: String? { - switch self { - case .failedToBuild(let product, let swiftPackageManagerError): - return "Failed to build '\(product)': \(swiftPackageManagerError.localizedDescription)'" - case .failedToCreateAppBundleDirectoryStructure(let bundleDirectory, _): - return "Failed to create app bundle directory structure at '\(bundleDirectory)'" - case .failedToCreatePkgInfo(let file, _): - return "Failed to create 'PkgInfo' file at '\(file)'" - case .failedToCreateInfoPlist(let plistCreatorError): - return "Failed to create 'Info.plist': \(plistCreatorError.localizedDescription)" - case .failedToCopyExecutable(let source, let destination, _): - return - "Failed to copy executable from '\(source.relativePath)' to '\(destination.relativePath)'" - case .failedToCreateIcon(let iconSetCreatorError): - return "Failed to create app icon: \(iconSetCreatorError.localizedDescription)" - case .failedToCopyICNS(let source, let destination, _): - return - "Failed to copy 'icns' file from '\(source.relativePath)' to '\(destination.relativePath)'" - case .failedToCopyResourceBundles(let resourceBundlerError): - return "Failed to copy resource bundles: \(resourceBundlerError.localizedDescription)" - case .failedToCopyDynamicLibraries(let dynamicLibraryBundlerError): - return - "Failed to copy dynamic libraries: \(dynamicLibraryBundlerError.localizedDescription)" - case .failedToRunExecutable(let processError): - return "Failed to run app executable: \(processError.localizedDescription)" - case .invalidAppIconFile(let file): - return "Invalid app icon file, must be 'png' or 'icns', got '\(file.relativePath)'" - case .failedToCopyProvisioningProfile: - return "Failed to copy provisioning profile to output bundle" - case .failedToCodesign(let error): - return error.localizedDescription - case .mustSpecifyBundleIdentifier: - return "Bundle identifier must be specified for iOS apps" - case .failedToLoadManifest(let error): - return error.localizedDescription - case .failedToGetMinimumIOSVersion(let manifest): - return - "To build for iOS, please specify a iOS deployment version in the platforms field of '\(manifest.relativePath)'" - } - } -} diff --git a/Sources/swift-bundler/Bundler/IconSetCreator.swift b/Sources/swift-bundler/Bundler/IconSetCreator.swift index d54bbde..1dec969 100644 --- a/Sources/swift-bundler/Bundler/IconSetCreator.swift +++ b/Sources/swift-bundler/Bundler/IconSetCreator.swift @@ -5,17 +5,18 @@ enum IconSetCreator { /// Creates an `AppIcon.icns` in the given directory from the given 1024x1024 input png. /// - Parameters: /// - icon: The 1024x1024 input icon. Must be a png. An error is returned if the icon's path extension is not `png` (case insensitive). - /// - outputDirectory: The output directory to put the generated `AppIcon.icns` in. + /// - outputFile: The location of the output `icns` file. /// - Returns: If an error occurs, a failure is returned. static func createIcns( from icon: URL, - outputDirectory: URL + outputFile: URL ) -> Result { guard icon.pathExtension.lowercased() == "png" else { return .failure(.notPNG(icon)) } - let iconSet = outputDirectory.appendingPathComponent("AppIcon.iconset") + let temporaryDirectory = FileManager.default.temporaryDirectory + let iconSet = temporaryDirectory.appendingPathComponent("AppIcon.iconset") do { try FileManager.default.createDirectory(at: iconSet) } catch { @@ -39,8 +40,8 @@ enum IconSetCreator { let process = Process.create( "/usr/bin/iconutil", - arguments: ["-c", "icns", iconSet.path], - directory: outputDirectory) + arguments: ["--convert", "icns", "--output", outputFile.path, iconSet.path] + ) if case let .failure(error) = process.runAndWait() { return .failure(.failedToConvertToICNS(error)) } diff --git a/Sources/swift-bundler/Bundler/MacOSBundler.swift b/Sources/swift-bundler/Bundler/MacOSBundler.swift deleted file mode 100644 index d5377dd..0000000 --- a/Sources/swift-bundler/Bundler/MacOSBundler.swift +++ /dev/null @@ -1,252 +0,0 @@ -import Foundation - -/// The bundler for creating macOS apps. -enum MacOSBundler: Bundler { - /// Bundles the built executable and resources into a macOS app. - /// - /// ``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 macOS builds. - /// - Returns: If a failure occurs, it is returned. - 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 { - log.info("Bundling '\(appName).app'") - - let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product) - - let appBundle = outputDirectory.appendingPathComponent("\(appName).app") - let appContents = appBundle.appendingPathComponent("Contents") - let appExecutable = appContents.appendingPathComponent("MacOS/\(appName)") - let appResources = appContents.appendingPathComponent("Resources") - let appDynamicLibrariesDirectory = appContents.appendingPathComponent("Libraries") - - let createAppIconIfPresent: () -> Result = { - if let path = appConfiguration.icon { - let icon = packageDirectory.appendingPathComponent(path) - return Self.createAppIcon(icon: icon, outputDirectory: appResources) - } - return .success() - } - - let copyResourcesBundles: () -> Result = { - ResourceBundler.copyResources( - from: productsDirectory, - to: appResources, - fixBundles: !isXcodeBuild && !universal, - platform: .macOS, - platformVersion: platformVersion, - packageName: packageName, - productName: appConfiguration.product - ).mapError { error in - .failedToCopyResourceBundles(error) - } - } - - let copyDynamicLibraries: () -> Result = { - DynamicLibraryBundler.copyDynamicLibraries( - dependedOnBy: appExecutable, - to: appDynamicLibrariesDirectory, - productsDirectory: productsDirectory, - isXcodeBuild: isXcodeBuild, - universal: universal, - makeStandAlone: standAlone - ).mapError { error in - .failedToCopyDynamicLibraries(error) - } - } - - let sign: () -> Result = { - if let identity = codesigningIdentity { - return CodeSigner.signAppBundle( - bundle: appBundle, - identityId: identity, - entitlements: codesigningEntitlements - ).mapError { error in - return .failedToCodesign(error) - } - } else { - return .success() - } - } - - let bundleApp = flatten( - { Self.createAppDirectoryStructure(at: outputDirectory, appName: appName) }, - { Self.copyExecutable(at: executableArtifact, to: appExecutable) }, - { - Self.createMetadataFiles( - at: appContents, appName: appName, appConfiguration: appConfiguration, - macOSVersion: platformVersion) - }, - createAppIconIfPresent, - copyResourcesBundles, - copyDynamicLibraries, - sign - ) - - return bundleApp().mapError { (error: MacOSBundlerError) -> Error in - return error - } - } - - // MARK: Private methods - - /// Creates the directory structure for an app. - /// - /// Creates the following structure: - /// - /// - `AppName.app` - /// - `Contents` - /// - `MacOS` - /// - `Resources` - /// - `Libraries` - /// - /// If the app directory already exists, it is deleted before continuing. - /// - /// - Parameters: - /// - outputDirectory: The directory to output the app to. - /// - appName: The name of the app. - /// - Returns: A failure if directory creation fails. - private static func createAppDirectoryStructure( - at outputDirectory: URL, appName: String - ) - -> Result - { - log.info("Creating '\(appName).app'") - let fileManager = FileManager.default - - let appBundleDirectory = outputDirectory.appendingPathComponent("\(appName).app") - let appContents = appBundleDirectory.appendingPathComponent("Contents") - let appResources = appContents.appendingPathComponent("Resources") - let appMacOS = appContents.appendingPathComponent("MacOS") - let appDynamicLibrariesDirectory = appContents.appendingPathComponent("Libraries") - - do { - if fileManager.itemExists(at: appBundleDirectory, withType: .directory) { - try fileManager.removeItem(at: appBundleDirectory) - } - try fileManager.createDirectory(at: appResources) - try fileManager.createDirectory(at: appMacOS) - try fileManager.createDirectory(at: appDynamicLibrariesDirectory) - return .success() - } catch { - return .failure( - .failedToCreateAppBundleDirectoryStructure(bundleDirectory: appBundleDirectory, error)) - } - } - - /// Copies the built executable into the app bundle. - /// - Parameters: - /// - source: The location of the built executable. - /// - destination: The target location of the built executable (the file not the directory). - /// - Returns: If an error occus, a failure is returned. - private static func copyExecutable( - at source: URL, to destination: URL - ) -> Result< - Void, MacOSBundlerError - > { - log.info("Copying executable") - do { - try FileManager.default.copyItem(at: source, to: destination) - return .success() - } catch { - return .failure(.failedToCopyExecutable(source: source, destination: destination, error)) - } - } - - /// Creates an app's `PkgInfo` and `Info.plist` files. - /// - Parameters: - /// - outputDirectory: Should be the app's `Contents` directory. - /// - appName: The app's name. - /// - appConfiguration: The app's configuration. - /// - macOSVersion: The macOS version to target. - /// - Returns: If an error occurs, a failure is returned. - private static func createMetadataFiles( - at outputDirectory: URL, - appName: String, - appConfiguration: AppConfiguration, - macOSVersion: String - ) -> Result { - log.info("Creating 'PkgInfo'") - let pkgInfoFile = outputDirectory.appendingPathComponent("PkgInfo") - do { - var pkgInfoBytes: [UInt8] = [0x41, 0x50, 0x50, 0x4c, 0x3f, 0x3f, 0x3f, 0x3f] - let pkgInfoData = Data(bytes: &pkgInfoBytes, count: pkgInfoBytes.count) - try pkgInfoData.write(to: pkgInfoFile) - } catch { - return .failure(.failedToCreatePkgInfo(file: pkgInfoFile, error)) - } - - log.info("Creating 'Info.plist'") - let infoPlistFile = outputDirectory.appendingPathComponent("Info.plist") - return PlistCreator.createAppInfoPlist( - at: infoPlistFile, - appName: appName, - configuration: appConfiguration, - platform: .macOS, - platformVersion: macOSVersion - ).mapError { error in - .failedToCreateInfoPlist(error) - } - } - - /// If given an `icns`, the `icns` gets copied to the output directory. If given a `png`, an `AppIcon.icns` is created from the `png`. - /// - /// The files are not validated any further than checking their file extensions. - /// - Parameters: - /// - icon: The app's icon. Should be either an `icns` file or a 1024x1024 `png` with an alpha channel. - /// - outputDirectory: Should be the app's `Resources` directory. - /// - Returns: If the png exists and there is an error while converting it to `icns`, a failure is returned. - /// If the file is neither an `icns` or a `png`, a failure is also returned. - private static func createAppIcon( - icon: URL, outputDirectory: URL - ) -> Result< - Void, MacOSBundlerError - > { - // Copy `AppIcon.icns` if present - if icon.pathExtension == "icns" { - log.info("Copying '\(icon.lastPathComponent)'") - let destination = outputDirectory.appendingPathComponent("AppIcon.icns") - do { - try FileManager.default.copyItem(at: icon, to: destination) - return .success() - } catch { - return .failure(.failedToCopyICNS(source: icon, destination: destination, error)) - } - } else if icon.pathExtension == "png" { - log.info("Creating 'AppIcon.icns' from '\(icon.lastPathComponent)'") - return IconSetCreator.createIcns(from: icon, outputDirectory: outputDirectory) - .mapError { error in - .failedToCreateIcon(error) - } - } - - return .failure(.invalidAppIconFile(icon)) - } -} diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/AppleOS.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/AppleOS.swift new file mode 100644 index 0000000..0ddddcb --- /dev/null +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/AppleOS.swift @@ -0,0 +1,9 @@ +import Foundation + +/// An Apple OS to build for. +enum AppleOS: String, CaseIterable { + case macOS + case iOS + case visionOS + case tvOS +} diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/ApplePlatform.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/ApplePlatform.swift new file mode 100644 index 0000000..eea9da3 --- /dev/null +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/ApplePlatform.swift @@ -0,0 +1,43 @@ +import Foundation + +/// An Apple platform to build for. +enum ApplePlatform: String, CaseIterable { + case macOS + case iOS + case iOSSimulator + case visionOS + case visionOSSimulator + case tvOS + case tvOSSimulator + + /// The platform's os (e.g. ``ApplePlatform/iOS`` and ``ApplePlatform/iOSSimulator`` + /// are both ``AppleOS/iOS``). + var os: AppleOS { + switch self { + case .macOS: return .macOS + case .iOS, .iOSSimulator: return .iOS + case .visionOS, .visionOSSimulator: return .visionOS + case .tvOS, .tvOSSimulator: return .tvOS + } + } + + /// The underlying platform. + var platform: Platform { + switch self { + case .macOS: + return .macOS + case .iOS: + return .iOS + case .iOSSimulator: + return .iOSSimulator + case .visionOS: + return .visionOS + case .visionOSSimulator: + return .visionOSSimulator + case .tvOS: + return .tvOS + case .tvOSSimulator: + return .tvOSSimulator + } + } +} diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/OS.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/OS.swift new file mode 100644 index 0000000..8438140 --- /dev/null +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/OS.swift @@ -0,0 +1,10 @@ +import Foundation + +/// An OS to build for. +enum OS: String, CaseIterable { + case macOS + case iOS + case visionOS + case tvOS + case linux +} diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift index aab8322..d3b0576 100644 --- a/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift @@ -62,6 +62,46 @@ enum Platform: String, CaseIterable { } } + /// Gets the platform as an ``ApplePlatform`` if it is in fact an Apple + /// platform. + var asApplePlatform: ApplePlatform? { + switch self { + case .macOS: return .macOS + case .iOS: return .iOS + case .iOSSimulator: return .iOSSimulator + case .visionOS: return .visionOS + case .visionOSSimulator: return .visionOSSimulator + case .tvOS: return .tvOS + case .tvOSSimulator: return .tvOSSimulator + case .linux: return nil + } + } + + /// The platform's os (e.g. ``Platform/iOS`` and ``Platform/iOSSimulator`` + /// are both ``OS/iOS``). + var os: OS { + switch self { + case .macOS: return .macOS + case .iOS, .iOSSimulator: return .iOS + case .visionOS, .visionOSSimulator: return .visionOS + case .tvOS, .tvOSSimulator: return .tvOS + case .linux: return .linux + } + } + + /// A simple lossless conversion. + init(_ applePlatform: ApplePlatform) { + switch applePlatform { + case .macOS: self = .macOS + case .iOS: self = .iOS + case .iOSSimulator: self = .iOSSimulator + case .visionOS: self = .visionOS + case .visionOSSimulator: self = .visionOSSimulator + case .tvOS: self = .tvOS + case .tvOSSimulator: self = .tvOSSimulator + } + } + /// The platform that Swift Bundler is currently being run on. static var currentPlatform: Platform { #if os(macOS) @@ -73,7 +113,7 @@ enum Platform: String, CaseIterable { } extension Platform: Equatable { - public static func ==(lhs: Platform, rhs: AppleSDKPlatform) -> Bool { + public static func == (lhs: Platform, rhs: AppleSDKPlatform) -> Bool { lhs == rhs.platform } } diff --git a/Sources/swift-bundler/Bundler/TVOSBundler.swift b/Sources/swift-bundler/Bundler/TVOSBundler.swift deleted file mode 100644 index 5c32858..0000000 --- a/Sources/swift-bundler/Bundler/TVOSBundler.swift +++ /dev/null @@ -1,280 +0,0 @@ -import Foundation - -// TODO: Combine all the Darwin-platform bundlers into a single DarwinBundler since they're all -// so similar and have an incredible amount of duplicate code (this is literally all just copy -// pasted from IOSBundler with all the iOS stuff renamed to tvOS). - -/// The bundler for creating tvOS apps. -enum TVOSBundler: Bundler { - /// Bundles the built executable and resources into an tvOS app. - /// - /// ``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: Does nothing for tvOS. - /// - universal: Does nothing for tvOS. - /// - 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: If `true`, the app should be bundled for running on a simulator. - /// - Returns: If a failure occurs, it is returned. - 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 { - log.info("Bundling '\(appName).app'") - - let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product) - - let appBundle = outputDirectory.appendingPathComponent("\(appName).app") - let appExecutable = appBundle.appendingPathComponent(appName) - let appDynamicLibrariesDirectory = appBundle.appendingPathComponent("Libraries") - - // let createAppIconIfPresent: () -> Result = { - // if let path = appConfiguration.icon { - // let icon = packageDirectory.appendingPathComponent(path) - // return Self.createAppIcon(icon: icon, outputDirectory: appAssets) - // } - // return .success() - // } - - let copyResourcesBundles: () -> Result = { - ResourceBundler.copyResources( - from: productsDirectory, - to: appBundle, - fixBundles: !isXcodeBuild && !universal, - platform: targetingSimulator ? .tvOSSimulator : .tvOS, - platformVersion: platformVersion, - packageName: packageName, - productName: appConfiguration.product - ).mapError { error in - .failedToCopyResourceBundles(error) - } - } - - let copyDynamicLibraries: () -> Result = { - DynamicLibraryBundler.copyDynamicLibraries( - dependedOnBy: appExecutable, - to: appDynamicLibrariesDirectory, - productsDirectory: productsDirectory, - isXcodeBuild: false, - universal: false, - makeStandAlone: false - ).mapError { error in - .failedToCopyDynamicLibraries(error) - } - } - - let embedProfile: () -> Result = { - if let provisioningProfile = provisioningProfile { - return Self.embedProvisioningProfile(provisioningProfile, in: appBundle) - } else { - return .success() - } - } - - let codesign: () -> Result = { - if let identity = codesigningIdentity { - return CodeSigner.signWithGeneratedEntitlements( - bundle: appBundle, - identityId: identity, - bundleIdentifier: appConfiguration.identifier - ).mapError { error in - return .failedToCodesign(error) - } - } else { - return .success() - } - } - - // TODO: Why is app icon creation disabled? - let bundleApp = flatten( - { Self.createAppDirectoryStructure(at: outputDirectory, appName: appName) }, - { Self.copyExecutable(at: executableArtifact, to: appExecutable) }, - { - Self.createMetadataFiles( - at: appBundle, - appName: appName, - appConfiguration: appConfiguration, - tvOSVersion: platformVersion, - targetingSimulator: targetingSimulator - ) - }, - // { createAppIconIfPresent() }, - { copyResourcesBundles() }, - { copyDynamicLibraries() }, - { embedProfile() }, - { codesign() } - ) - - return bundleApp().mapError { (error: TVOSBundlerError) -> Error in - return error - } - } - - // MARK: Private methods - - /// Creates the directory structure for an app. - /// - /// Creates the following structure: - /// - /// - `AppName.app` - /// - `Libraries` - /// - /// If the app directory already exists, it is deleted before continuing. - /// - /// - Parameters: - /// - outputDirectory: The directory to output the app to. - /// - appName: The name of the app. - /// - Returns: A failure if directory creation fails. - private static func createAppDirectoryStructure( - at outputDirectory: URL, appName: String - ) - -> Result - { - log.info("Creating '\(appName).app'") - let fileManager = FileManager.default - - let appBundleDirectory = outputDirectory.appendingPathComponent("\(appName).app") - let appDynamicLibrariesDirectory = appBundleDirectory.appendingPathComponent("Libraries") - - do { - if fileManager.itemExists(at: appBundleDirectory, withType: .directory) { - try fileManager.removeItem(at: appBundleDirectory) - } - try fileManager.createDirectory(at: appBundleDirectory) - try fileManager.createDirectory(at: appDynamicLibrariesDirectory) - return .success() - } catch { - return .failure( - .failedToCreateAppBundleDirectoryStructure(bundleDirectory: appBundleDirectory, error)) - } - } - - /// Copies the built executable into the app bundle. - /// - Parameters: - /// - source: The location of the built executable. - /// - destination: The target location of the built executable (the file not the directory). - /// - Returns: If an error occus, a failure is returned. - private static func copyExecutable( - at source: URL, to destination: URL - ) -> Result< - Void, TVOSBundlerError - > { - log.info("Copying executable") - do { - try FileManager.default.copyItem(at: source, to: destination) - return .success() - } catch { - return .failure(.failedToCopyExecutable(source: source, destination: destination, error)) - } - } - - /// Creates an app's `PkgInfo` and `Info.plist` files. - /// - Parameters: - /// - outputDirectory: Should be the app's `Contents` directory. - /// - appName: The app's name. - /// - appConfiguration: The app's configuration. - /// - tvOSVersion: The tvOS version to target. - /// - targetingSimulator: If `true`, the files will be created for running in a simulator. - /// - Returns: If an error occurs, a failure is returned. - private static func createMetadataFiles( - at outputDirectory: URL, - appName: String, - appConfiguration: AppConfiguration, - tvOSVersion: String, - targetingSimulator: Bool - ) -> Result { - log.info("Creating 'PkgInfo'") - let pkgInfoFile = outputDirectory.appendingPathComponent("PkgInfo") - do { - var pkgInfoBytes: [UInt8] = [0x41, 0x50, 0x50, 0x4c, 0x3f, 0x3f, 0x3f, 0x3f] - let pkgInfoData = Data(bytes: &pkgInfoBytes, count: pkgInfoBytes.count) - try pkgInfoData.write(to: pkgInfoFile) - } catch { - return .failure(.failedToCreatePkgInfo(file: pkgInfoFile, error)) - } - - log.info("Creating 'Info.plist'") - let infoPlistFile = outputDirectory.appendingPathComponent("Info.plist") - return PlistCreator.createAppInfoPlist( - at: infoPlistFile, - appName: appName, - configuration: appConfiguration, - platform: targetingSimulator ? .tvOSSimulator : .tvOS, - platformVersion: tvOSVersion - ).mapError { error in - .failedToCreateInfoPlist(error) - } - } - - /// If given an `icns`, the `icns` gets copied to the output directory. If given a `png`, an `AppIcon.icns` is created from the `png`. - /// - /// The files are not validated any further than checking their file extensions. - /// - Parameters: - /// - icon: The app's icon. Should be either an `icns` file or a 1024x1024 `png` with an alpha channel. - /// - outputDirectory: Should be the app's `Resources` directory. - /// - Returns: If the png exists and there is an error while converting it to `icns`, a failure is returned. - /// If the file is neither an `icns` or a `png`, a failure is also returned. - private static func createAppIcon( - icon: URL, outputDirectory: URL - ) -> Result< - Void, TVOSBundlerError - > { - // Copy `AppIcon.icns` if present - if icon.pathExtension == "icns" { - log.info("Copying '\(icon.lastPathComponent)'") - let destination = outputDirectory.appendingPathComponent("AppIcon.icns") - do { - try FileManager.default.copyItem(at: icon, to: destination) - return .success() - } catch { - return .failure(.failedToCopyICNS(source: icon, destination: destination, error)) - } - } else if icon.pathExtension == "png" { - log.info("Creating 'AppIcon.icns' from '\(icon.lastPathComponent)'") - return IconSetCreator.createIcns(from: icon, outputDirectory: outputDirectory) - .mapError { error in - .failedToCreateIcon(error) - } - } - - return .failure(.invalidAppIconFile(icon)) - } - - private static func embedProvisioningProfile( - _ provisioningProfile: URL, in bundle: URL - ) - -> Result - { - log.info("Embedding provisioning profile") - - do { - try FileManager.default.copyItem( - at: provisioningProfile, to: bundle.appendingPathComponent("embedded.mobileprovision")) - } catch { - return .failure(.failedToCopyProvisioningProfile(error)) - } - - return .success() - } -} diff --git a/Sources/swift-bundler/Bundler/TVOSBundlerError.swift b/Sources/swift-bundler/Bundler/TVOSBundlerError.swift deleted file mode 100644 index dc50c99..0000000 --- a/Sources/swift-bundler/Bundler/TVOSBundlerError.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -/// An error returned by ``TVOSBundler``. -enum TVOSBundlerError: LocalizedError { - case failedToBuild(product: String, SwiftPackageManagerError) - case failedToCreateAppBundleDirectoryStructure(bundleDirectory: URL, Error) - case failedToCreatePkgInfo(file: URL, Error) - case failedToCreateInfoPlist(PlistCreatorError) - case failedToCopyExecutable(source: URL, destination: URL, Error) - case failedToCreateIcon(IconSetCreatorError) - case failedToCopyICNS(source: URL, destination: URL, Error) - case failedToCopyResourceBundles(ResourceBundlerError) - case failedToCopyDynamicLibraries(DynamicLibraryBundlerError) - case failedToRunExecutable(ProcessError) - case invalidAppIconFile(URL) - case failedToCopyProvisioningProfile(Error) - case failedToCodesign(CodeSignerError) - case mustSpecifyBundleIdentifier - case failedToLoadManifest(SwiftPackageManagerError) - case failedToGetMinimumTVOSVersion(manifest: URL) - - var errorDescription: String? { - switch self { - case .failedToBuild(let product, let swiftPackageManagerError): - return "Failed to build '\(product)': \(swiftPackageManagerError.localizedDescription)'" - case .failedToCreateAppBundleDirectoryStructure(let bundleDirectory, _): - return "Failed to create app bundle directory structure at '\(bundleDirectory)'" - case .failedToCreatePkgInfo(let file, _): - return "Failed to create 'PkgInfo' file at '\(file)'" - case .failedToCreateInfoPlist(let plistCreatorError): - return "Failed to create 'Info.plist': \(plistCreatorError.localizedDescription)" - case .failedToCopyExecutable(let source, let destination, _): - return - "Failed to copy executable from '\(source.relativePath)' to '\(destination.relativePath)'" - case .failedToCreateIcon(let iconSetCreatorError): - return "Failed to create app icon: \(iconSetCreatorError.localizedDescription)" - case .failedToCopyICNS(let source, let destination, _): - return - "Failed to copy 'icns' file from '\(source.relativePath)' to '\(destination.relativePath)'" - case .failedToCopyResourceBundles(let resourceBundlerError): - return "Failed to copy resource bundles: \(resourceBundlerError.localizedDescription)" - case .failedToCopyDynamicLibraries(let dynamicLibraryBundlerError): - return - "Failed to copy dynamic libraries: \(dynamicLibraryBundlerError.localizedDescription)" - case .failedToRunExecutable(let processError): - return "Failed to run app executable: \(processError.localizedDescription)" - case .invalidAppIconFile(let file): - return "Invalid app icon file, must be 'png' or 'icns', got '\(file.relativePath)'" - case .failedToCopyProvisioningProfile: - return "Failed to copy provisioning profile to output bundle" - case .failedToCodesign(let error): - return error.localizedDescription - case .mustSpecifyBundleIdentifier: - return "Bundle identifier must be specified for tvOS apps" - case .failedToLoadManifest(let error): - return error.localizedDescription - case .failedToGetMinimumTVOSVersion(let manifest): - return - "To build for tvOS, please specify a tvOS deployment version in the platforms field of '\(manifest.relativePath)'" - } - } -} diff --git a/Sources/swift-bundler/Bundler/VisionOSBundler.swift b/Sources/swift-bundler/Bundler/VisionOSBundler.swift deleted file mode 100644 index 0be0d45..0000000 --- a/Sources/swift-bundler/Bundler/VisionOSBundler.swift +++ /dev/null @@ -1,276 +0,0 @@ -import Foundation - -/// The bundler for creating visionOS apps. -enum VisionOSBundler: Bundler { - /// Bundles the built executable and resources into an visionOS app. - /// - /// ``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: Does nothing for visionOS. - /// - universal: Does nothing for visionOS. - /// - 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: If `true`, the app should be bundled for running on a simulator. - /// - Returns: If a failure occurs, it is returned. - 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 { - log.info("Bundling '\(appName).app'") - - let executableArtifact = productsDirectory.appendingPathComponent(appConfiguration.product) - - let appBundle = outputDirectory.appendingPathComponent("\(appName).app") - let appExecutable = appBundle.appendingPathComponent(appName) - let appDynamicLibrariesDirectory = appBundle.appendingPathComponent("Libraries") - - // let createAppIconIfPresent: () -> Result = { - // if let path = appConfiguration.icon { - // let icon = packageDirectory.appendingPathComponent(path) - // return Self.createAppIcon(icon: icon, outputDirectory: appAssets) - // } - // return .success() - // } - - let copyResourcesBundles: () -> Result = { - ResourceBundler.copyResources( - from: productsDirectory, - to: appBundle, - fixBundles: !isXcodeBuild && !universal, - platform: targetingSimulator ? .visionOSSimulator : .visionOS, - platformVersion: platformVersion, - packageName: packageName, - productName: appConfiguration.product - ).mapError { error in - .failedToCopyResourceBundles(error) - } - } - - let copyDynamicLibraries: () -> Result = { - DynamicLibraryBundler.copyDynamicLibraries( - dependedOnBy: appExecutable, - to: appDynamicLibrariesDirectory, - productsDirectory: productsDirectory, - isXcodeBuild: false, - universal: false, - makeStandAlone: false - ).mapError { error in - .failedToCopyDynamicLibraries(error) - } - } - - let embedProfile: () -> Result = { - if let provisioningProfile = provisioningProfile { - return embedProvisioningProfile(provisioningProfile, in: appBundle) - } else { - return .success() - } - } - - let codesign: () -> Result = { - if let identity = codesigningIdentity { - return CodeSigner.signWithGeneratedEntitlements( - bundle: appBundle, - identityId: identity, - bundleIdentifier: appConfiguration.identifier - ).mapError { error in - .failedToCodesign(error) - } - } else { - return .success() - } - } - - // TODO: Why is app icon creation disabled? - let bundleApp = flatten( - { createAppDirectoryStructure(at: outputDirectory, appName: appName) }, - { copyExecutable(at: executableArtifact, to: appExecutable) }, - { - createMetadataFiles( - at: appBundle, - appName: appName, - appConfiguration: appConfiguration, - visionOSVersion: platformVersion, - targetingSimulator: targetingSimulator - ) - }, - // { createAppIconIfPresent() }, - { copyResourcesBundles() }, - { copyDynamicLibraries() }, - { embedProfile() }, - { codesign() } - ) - - return bundleApp().mapError { (error: VisionOSBundlerError) -> Error in - error - } - } - - // MARK: Private methods - - /// Creates the directory structure for an app. - /// - /// Creates the following structure: - /// - /// - `AppName.app` - /// - `Libraries` - /// - /// If the app directory already exists, it is deleted before continuing. - /// - /// - Parameters: - /// - outputDirectory: The directory to output the app to. - /// - appName: The name of the app. - /// - Returns: A failure if directory creation fails. - private static func createAppDirectoryStructure( - at outputDirectory: URL, appName: String - ) - -> Result - { - log.info("Creating '\(appName).app'") - let fileManager = FileManager.default - - let appBundleDirectory = outputDirectory.appendingPathComponent("\(appName).app") - let appDynamicLibrariesDirectory = appBundleDirectory.appendingPathComponent("Libraries") - - do { - if fileManager.itemExists(at: appBundleDirectory, withType: .directory) { - try fileManager.removeItem(at: appBundleDirectory) - } - try fileManager.createDirectory(at: appBundleDirectory) - try fileManager.createDirectory(at: appDynamicLibrariesDirectory) - return .success() - } catch { - return .failure( - .failedToCreateAppBundleDirectoryStructure(bundleDirectory: appBundleDirectory, error)) - } - } - - /// Copies the built executable into the app bundle. - /// - Parameters: - /// - source: The location of the built executable. - /// - destination: The target location of the built executable (the file not the directory). - /// - Returns: If an error occus, a failure is returned. - private static func copyExecutable( - at source: URL, to destination: URL - ) -> Result< - Void, VisionOSBundlerError - > { - log.info("Copying executable") - do { - try FileManager.default.copyItem(at: source, to: destination) - return .success() - } catch { - return .failure(.failedToCopyExecutable(source: source, destination: destination, error)) - } - } - - /// Creates an app's `PkgInfo` and `Info.plist` files. - /// - Parameters: - /// - outputDirectory: Should be the app's `Contents` directory. - /// - appName: The app's name. - /// - appConfiguration: The app's configuration. - /// - visionOSVersion: The visionOS version to target. - /// - targetingSimulator: If `true`, the files will be created for running in a simulator. - /// - Returns: If an error occurs, a failure is returned. - private static func createMetadataFiles( - at outputDirectory: URL, - appName: String, - appConfiguration: AppConfiguration, - visionOSVersion: String, - targetingSimulator: Bool - ) -> Result { - log.info("Creating 'PkgInfo'") - let pkgInfoFile = outputDirectory.appendingPathComponent("PkgInfo") - do { - var pkgInfoBytes: [UInt8] = [0x41, 0x50, 0x50, 0x4C, 0x3F, 0x3F, 0x3F, 0x3F] - let pkgInfoData = Data(bytes: &pkgInfoBytes, count: pkgInfoBytes.count) - try pkgInfoData.write(to: pkgInfoFile) - } catch { - return .failure(.failedToCreatePkgInfo(file: pkgInfoFile, error)) - } - - log.info("Creating 'Info.plist'") - let infoPlistFile = outputDirectory.appendingPathComponent("Info.plist") - return PlistCreator.createAppInfoPlist( - at: infoPlistFile, - appName: appName, - configuration: appConfiguration, - platform: targetingSimulator ? .visionOSSimulator : .visionOS, - platformVersion: visionOSVersion - ).mapError { error in - .failedToCreateInfoPlist(error) - } - } - - /// If given an `icns`, the `icns` gets copied to the output directory. If given a `png`, an `AppIcon.icns` is created from the `png`. - /// - /// The files are not validated any further than checking their file extensions. - /// - Parameters: - /// - icon: The app's icon. Should be either an `icns` file or a 1024x1024 `png` with an alpha channel. - /// - outputDirectory: Should be the app's `Resources` directory. - /// - Returns: If the png exists and there is an error while converting it to `icns`, a failure is returned. - /// If the file is neither an `icns` or a `png`, a failure is also returned. - private static func createAppIcon( - icon: URL, outputDirectory: URL - ) -> Result< - Void, VisionOSBundlerError - > { - // Copy `AppIcon.icns` if present - if icon.pathExtension == "icns" { - log.info("Copying '\(icon.lastPathComponent)'") - let destination = outputDirectory.appendingPathComponent("AppIcon.icns") - do { - try FileManager.default.copyItem(at: icon, to: destination) - return .success() - } catch { - return .failure(.failedToCopyICNS(source: icon, destination: destination, error)) - } - } else if icon.pathExtension == "png" { - log.info("Creating 'AppIcon.icns' from '\(icon.lastPathComponent)'") - return IconSetCreator.createIcns(from: icon, outputDirectory: outputDirectory) - .mapError { error in - .failedToCreateIcon(error) - } - } - - return .failure(.invalidAppIconFile(icon)) - } - - private static func embedProvisioningProfile( - _ provisioningProfile: URL, in bundle: URL - ) - -> Result - { - log.info("Embedding provisioning profile") - - do { - try FileManager.default.copyItem( - at: provisioningProfile, to: bundle.appendingPathComponent("embedded.mobileprovision")) - } catch { - return .failure(.failedToCopyProvisioningProfile(error)) - } - - return .success() - } -} diff --git a/Sources/swift-bundler/Bundler/VisionOSBundlerError.swift b/Sources/swift-bundler/Bundler/VisionOSBundlerError.swift deleted file mode 100644 index b85262a..0000000 --- a/Sources/swift-bundler/Bundler/VisionOSBundlerError.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Foundation - -/// An error returned by ``VisionOSBundler``. -enum VisionOSBundlerError: LocalizedError { - case failedToBuild(product: String, SwiftPackageManagerError) - case failedToCreateAppBundleDirectoryStructure(bundleDirectory: URL, Error) - case failedToCreatePkgInfo(file: URL, Error) - case failedToCreateInfoPlist(PlistCreatorError) - case failedToCopyExecutable(source: URL, destination: URL, Error) - case failedToCreateIcon(IconSetCreatorError) - case failedToCopyICNS(source: URL, destination: URL, Error) - case failedToCopyResourceBundles(ResourceBundlerError) - case failedToCopyDynamicLibraries(DynamicLibraryBundlerError) - case failedToRunExecutable(ProcessError) - case invalidAppIconFile(URL) - case failedToCopyProvisioningProfile(Error) - case failedToCodesign(CodeSignerError) - case mustSpecifyBundleIdentifier - case failedToLoadManifest(SwiftPackageManagerError) - case failedToGetMinimumVisionOSVersion(manifest: URL) - - var errorDescription: String? { - switch self { - case let .failedToBuild(product, swiftPackageManagerError): - return "Failed to build '\(product)': \(swiftPackageManagerError.localizedDescription)'" - case let .failedToCreateAppBundleDirectoryStructure(bundleDirectory, _): - return "Failed to create app bundle directory structure at '\(bundleDirectory)'" - case let .failedToCreatePkgInfo(file, _): - return "Failed to create 'PkgInfo' file at '\(file)'" - case let .failedToCreateInfoPlist(plistCreatorError): - return "Failed to create 'Info.plist': \(plistCreatorError.localizedDescription)" - case let .failedToCopyExecutable(source, destination, _): - return - "Failed to copy executable from '\(source.relativePath)' to '\(destination.relativePath)'" - case let .failedToCreateIcon(iconSetCreatorError): - return "Failed to create app icon: \(iconSetCreatorError.localizedDescription)" - case let .failedToCopyICNS(source, destination, _): - return - "Failed to copy 'icns' file from '\(source.relativePath)' to '\(destination.relativePath)'" - case let .failedToCopyResourceBundles(resourceBundlerError): - return "Failed to copy resource bundles: \(resourceBundlerError.localizedDescription)" - case let .failedToCopyDynamicLibraries(dynamicLibraryBundlerError): - return - "Failed to copy dynamic libraries: \(dynamicLibraryBundlerError.localizedDescription)" - case let .failedToRunExecutable(processError): - return "Failed to run app executable: \(processError.localizedDescription)" - case let .invalidAppIconFile(file): - return "Invalid app icon file, must be 'png' or 'icns', got '\(file.relativePath)'" - case .failedToCopyProvisioningProfile: - return "Failed to copy provisioning profile to output bundle" - case let .failedToCodesign(error): - return error.localizedDescription - case .mustSpecifyBundleIdentifier: - return "Bundle identifier must be specified for visionOS apps" - case let .failedToLoadManifest(error): - return error.localizedDescription - case let .failedToGetMinimumVisionOSVersion(manifest): - return - "To build for visionOS, please specify a visionOS deployment version in the platforms field of '\(manifest.relativePath)'" - } - } -} diff --git a/Sources/swift-bundler/Commands/BundleCommand.swift b/Sources/swift-bundler/Commands/BundleCommand.swift index 69c1eca..94315a3 100644 --- a/Sources/swift-bundler/Commands/BundleCommand.swift +++ b/Sources/swift-bundler/Commands/BundleCommand.swift @@ -228,24 +228,42 @@ struct BundleCommand: AsyncCommand { } // Create bundle job - let bundler = getBundler(for: arguments.platform) + let bundlerContext = BundlerContext( + appName: appName, + packageName: manifest.displayName, + appConfiguration: appConfiguration, + packageDirectory: packageDirectory, + productsDirectory: productsDirectory, + outputDirectory: outputDirectory, + platform: arguments.platform + ) let bundle = { - bundler.bundle( - appName: appName, - packageName: manifest.displayName, - appConfiguration: appConfiguration, - packageDirectory: packageDirectory, - productsDirectory: productsDirectory, - outputDirectory: outputDirectory, - isXcodeBuild: builtWithXcode, - universal: universal, - standAlone: arguments.standAlone, - codesigningIdentity: arguments.identity, - codesigningEntitlements: arguments.entitlements, - provisioningProfile: arguments.provisioningProfile, - platformVersion: platformVersion, - targetingSimulator: arguments.platform.isSimulator - ) + if let applePlatform = arguments.platform.asApplePlatform { + let codeSigningContext: DarwinBundler.Context.CodeSigningContext? + if let identity = arguments.identity { + codeSigningContext = DarwinBundler.Context.CodeSigningContext( + identity: identity, + entitlements: arguments.entitlements, + provisioningProfile: arguments.provisioningProfile + ) + } else { + codeSigningContext = nil + } + + return DarwinBundler.bundle( + bundlerContext, + DarwinBundler.Context( + isXcodeBuild: builtWithXcode, + universal: universal, + standAlone: arguments.standAlone, + platform: applePlatform, + platformVersion: platformVersion, + codeSigningContext: codeSigningContext + ) + ).intoAnyError() + } else { + return AppImageBundler.bundle(bundlerContext, ()).intoAnyError() + } } // Build pipeline diff --git a/Sources/swift-bundler/Configuration/PackageConfiguration.swift b/Sources/swift-bundler/Configuration/PackageConfiguration.swift index 4dc5dbd..c8d2215 100644 --- a/Sources/swift-bundler/Configuration/PackageConfiguration.swift +++ b/Sources/swift-bundler/Configuration/PackageConfiguration.swift @@ -78,19 +78,24 @@ struct PackageConfiguration: Codable { } } catch { // Maybe the configuration is a Swift Bundler v2 configuration. Attempt to migrate it. - migrationAttempt: do { + do { let table = try TOMLTable(string: contents) guard !table.contains(key: CodingKeys.formatVersion.rawValue) else { - break migrationAttempt + return .failure(.failedToDeserializeConfiguration(error)) } - return migrateV2Configuration( + switch migrateV2Configuration( configurationFile, mode: migrateConfiguration ? .writeChanges(backup: true) : .readOnly - ) - } catch {} - - return .failure(.failedToDeserializeConfiguration(error)) + ) { + case let .failure(error): + return .failure(error) + case let .success(config): + configuration = config + } + } catch { + return .failure(.failedToDeserializeConfiguration(error)) + } } if configuration.formatVersion != PackageConfiguration.currentFormatVersion {