From c01c4e0fd8c0563290eabc627cc33af37a0cb620 Mon Sep 17 00:00:00 2001 From: Erik Bautista Santibanez Date: Thu, 26 Dec 2024 10:38:00 -0800 Subject: [PATCH] feat: allow bundling 'darwinApp' platforms from non-mac hosts --- .../SwiftPackageManager/Platform.swift | 9 -- .../SwiftPackageManager.swift | 117 ++++++++++-------- .../Commands/BundleArguments.swift | 32 ++--- .../Commands/BundleCommand.swift | 41 +++--- .../Commands/BundlerChoice.swift | 34 +++-- Sources/swift-bundler/SwiftBundler.swift | 28 ++++- 6 files changed, 143 insertions(+), 118 deletions(-) diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift index 5f3ecb49..5229c9b8 100644 --- a/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/Platform.swift @@ -101,15 +101,6 @@ enum Platform: String, CaseIterable { case .tvOSSimulator: self = .tvOSSimulator } } - - /// The platform that Swift Bundler is currently being run on. - static var host: Platform { - #if os(macOS) - return .macOS - #elseif os(Linux) - return .linux - #endif - } } extension Platform: Equatable { diff --git a/Sources/swift-bundler/Bundler/SwiftPackageManager/SwiftPackageManager.swift b/Sources/swift-bundler/Bundler/SwiftPackageManager/SwiftPackageManager.swift index d277b3b3..08125114 100644 --- a/Sources/swift-bundler/Bundler/SwiftPackageManager/SwiftPackageManager.swift +++ b/Sources/swift-bundler/Bundler/SwiftPackageManager/SwiftPackageManager.swift @@ -236,46 +236,53 @@ enum SwiftPackageManager { let platformArguments: [String] switch buildContext.platform { case .iOS, .visionOS, .tvOS, .iOSSimulator, .visionOSSimulator, .tvOSSimulator: - let sdkPath: String - switch getLatestSDKPath(for: buildContext.platform) { - case .success(let path): - sdkPath = path - case .failure(let error): - return .failure(error) - } + #if os(macOS) + let sdkPath: String + switch getLatestSDKPath(for: buildContext.platform) { + case .success(let path): + sdkPath = path + case .failure(let error): + return .failure(error) + } - guard let platformVersion = buildContext.platformVersion else { - return .failure(.missingDarwinPlatformVersion(buildContext.platform)) - } - let hostArchitecture = BuildArchitecture.current - - let targetTriple: LLVMTargetTriple - switch buildContext.platform { - case .iOS: - targetTriple = .apple(.arm64, .iOS(platformVersion)) - case .visionOS: - targetTriple = .apple(.arm64, .visionOS(platformVersion)) - case .tvOS: - targetTriple = .apple(.arm64, .tvOS(platformVersion)) - case .iOSSimulator: - targetTriple = .apple(hostArchitecture, .iOS(platformVersion), .simulator) - case .visionOSSimulator: - targetTriple = .apple(hostArchitecture, .visionOS(platformVersion), .simulator) - case .tvOSSimulator: - targetTriple = .apple(hostArchitecture, .tvOS(platformVersion), .simulator) - default: - fatalError("Unreachable (supposedly)") - } + guard let platformVersion = buildContext.platformVersion else { + return .failure(.missingDarwinPlatformVersion(buildContext.platform)) + } + let hostArchitecture = BuildArchitecture.current + + let targetTriple: LLVMTargetTriple + switch buildContext.platform { + case .iOS: + targetTriple = .apple(.arm64, .iOS(platformVersion)) + case .visionOS: + targetTriple = .apple(.arm64, .visionOS(platformVersion)) + case .tvOS: + targetTriple = .apple(.arm64, .tvOS(platformVersion)) + case .iOSSimulator: + targetTriple = .apple(hostArchitecture, .iOS(platformVersion), .simulator) + case .visionOSSimulator: + targetTriple = .apple(hostArchitecture, .visionOS(platformVersion), .simulator) + case .tvOSSimulator: + targetTriple = .apple(hostArchitecture, .tvOS(platformVersion), .simulator) + default: + fatalError("Unreachable (supposedly)") + } - platformArguments = - [ - "-sdk", sdkPath, - "-target", targetTriple.description, - ].flatMap { ["-Xswiftc", $0] } - + [ - "--target=\(targetTriple)", - "-isysroot", sdkPath, - ].flatMap { ["-Xcc", $0] } + // TODO: Should this be omitted on non-mac hosts? + // adding '-target' requires setting '-sdk', which can be retrieved on mac platforms via `xcrun`, but if using + // cross-compilation how do we get the SDK path? + platformArguments = + [ + "-sdk", sdkPath, + "-target", targetTriple.description, + ].flatMap { ["-Xswiftc", $0] } + + [ + "--target=\(targetTriple)", + "-isysroot", sdkPath, + ].flatMap { ["-Xcc", $0] } + #else + platformArguments = [] + #endif case .macOS, .linux: platformArguments = [] } @@ -300,22 +307,26 @@ enum SwiftPackageManager { return .success(arguments) } - /// Gets the path to the latest SDK for a given platform. - /// - Parameter platform: The platform to get the SDK path for. - /// - Returns: The SDK's path, or a failure if an error occurs. - static func getLatestSDKPath(for platform: Platform) -> Result { - return Process.create( - "/usr/bin/xcrun", - arguments: [ - "--sdk", platform.sdkName, - "--show-sdk-path", - ] - ).getOutput().map { output in - return output.trimmingCharacters(in: .whitespacesAndNewlines) - }.mapError { error in - return .failedToGetLatestSDKPath(platform, error) + /// xcrun is only available on macOS. + #if os(macOS) + /// Gets the path to the latest SDK for a given platform. + /// - Parameter platform: The platform to get the SDK path for. + /// - Returns: The SDK's path, or a failure if an error occurs. + static func getLatestSDKPath(for platform: Platform) -> Result + { + return Process.create( + "/usr/bin/xcrun", + arguments: [ + "--sdk", platform.sdkName, + "--show-sdk-path", + ] + ).getOutput().map { output in + return output.trimmingCharacters(in: .whitespacesAndNewlines) + }.mapError { error in + return .failedToGetLatestSDKPath(platform, error) + } } - } + #endif /// Gets the version of the current Swift installation. /// - Returns: The swift version, or a failure if an error occurs. diff --git a/Sources/swift-bundler/Commands/BundleArguments.swift b/Sources/swift-bundler/Commands/BundleArguments.swift index 92934d48..1b3ab1b5 100644 --- a/Sources/swift-bundler/Commands/BundleArguments.swift +++ b/Sources/swift-bundler/Commands/BundleArguments.swift @@ -8,7 +8,10 @@ struct BundleArguments: ParsableArguments { var appName: String? @Option( - help: "The bundler to use \(BundlerChoice.possibleValuesDescription).", + help: """ + The bundler to use \(BundlerChoice.possibleValuesDescription). \ + (default: \(BundlerChoice.defaultForHostPlatform)) + """, transform: { guard let choice = BundlerChoice(rawValue: $0) else { throw CLIError.invalidBundlerChoice($0) @@ -70,11 +73,10 @@ struct BundleArguments: ParsableArguments { @Option( name: [.customShort("a"), .customLong("arch")], parsing: .singleValue, - help: { - let possibleValues = BuildArchitecture.possibleValuesDescription - let defaultValue = BuildArchitecture.current.rawValue - return "The architectures to build for \(possibleValues). (default: [\(defaultValue)])" - }(), + help: """ + The architectures to build for \(BuildArchitecture.possibleValuesDescription). \ + (default: [\(BuildArchitecture.current.rawValue)]) + """, transform: { string in guard let arch = BuildArchitecture.init(rawValue: string) else { throw CLIError.invalidArchitecture(string) @@ -100,10 +102,10 @@ struct BundleArguments: ParsableArguments { /// The platform to build for. @Option( name: .shortAndLong, - help: { - let possibleValues = Platform.possibleValuesDescription - return "The platform to build for \(possibleValues). (default: macOS)" - }(), + help: """ + The platform to build for \(Platform.possibleValuesDescription). \ + (default: \(SwiftBundler.host.defaultForHostPlatform)) + """, transform: { string in // also support getting a platform by its apple sdk equivalent. if let appleSDK = AppleSDKPlatform(rawValue: string) { @@ -115,12 +117,14 @@ struct BundleArguments: ParsableArguments { } return platform }) - var platform = Platform.host + var platform = SwiftBundler.host.defaultForHostPlatform /// A codesigning identity to use. - @Option( - name: .customLong("identity"), - help: "The identity to use for codesigning") + #if os(macOS) + @Option( + name: .customLong("identity"), + help: "The identity to use for codesigning") + #endif var identity: String? /// A provisioning profile to use. diff --git a/Sources/swift-bundler/Commands/BundleCommand.swift b/Sources/swift-bundler/Commands/BundleCommand.swift index 207c2d1c..78a5f528 100644 --- a/Sources/swift-bundler/Commands/BundleCommand.swift +++ b/Sources/swift-bundler/Commands/BundleCommand.swift @@ -78,33 +78,28 @@ struct BundleCommand: AsyncCommand { } #endif - if Platform.host == .linux && platform != .linux { - log.error("'--platform \(platform)' is not supported on Linux") - return false - } - - guard arguments.bundler.isSupportedOnHostPlatform else { - log.error( - """ - The '\(arguments.bundler.rawValue)' bundler is not supported on the \ - current host platform. Supported values: \ - \(BundlerChoice.supportedHostValuesDescription) - """ - ) - return false - } + // TODO: Should this be removed now that cross-compilation works? + // guard arguments.bundler.isSupportedOnHostPlatform else { + // log.error( + // """ + // The '\(arguments.bundler.rawValue)' bundler is not supported on the \ + // current host platform. Supported values: \ + // \(BundlerChoice.supportedHostValuesDescription) + // """ + // ) + // return false + // } guard arguments.bundler.supportedTargetPlatforms.contains(platform) else { - let alternatives = BundlerChoice.allCases.filter { choice in - choice.supportedTargetPlatforms.contains(platform) - } - let alternativesDescription = "(\(alternatives.map(\.rawValue).joined(separator: "|")))" + let alternativesDescription = + "(\(BundlerChoice.allCases.map(\.rawValue).joined(separator: "|")))" + let bundleTargetsDescription = + "(\(arguments.bundler.supportedTargetPlatforms.map(\.rawValue).joined(separator: "|")))" log.error( """ - The '\(arguments.bundler.rawValue)' bundler doesn't support bundling \ - for '\(platform)'. Supported target platforms: \ - \(BundlerChoice.supportedHostValuesDescription). Valid alternative \ - bundlers: \(alternativesDescription) + The '\(arguments.bundler.rawValue)' bundler doesn't support bundling for '\(platform)'. + * Supported platforms for bundler '\(arguments.bundler.rawValue)': \(bundleTargetsDescription). + * Alternative bundlers: \(alternativesDescription) """ ) return false diff --git a/Sources/swift-bundler/Commands/BundlerChoice.swift b/Sources/swift-bundler/Commands/BundlerChoice.swift index 623b10b6..841853db 100644 --- a/Sources/swift-bundler/Commands/BundlerChoice.swift +++ b/Sources/swift-bundler/Commands/BundlerChoice.swift @@ -20,16 +20,14 @@ enum BundlerChoice: String, CaseIterable { } /// Whether the choice is supported on the host platform. - var isSupportedOnHostPlatform: Bool { - supportedHostPlatforms.contains(Platform.host) - } + // var isSupportedOnHostPlatform: Bool { + // supportedHostPlatforms.contains(SwiftBundler.host.defaultForHostPlatform) + // } /// The default choice for the host platform. static var defaultForHostPlatform: Self { - // TODO: Give Platform.host its own type so that we don't have to pretend - // that iOS is a valid host platform (for now...) - switch Platform.host { - case .macOS, .iOS, .iOSSimulator, .tvOS, .tvOSSimulator, .visionOS, .visionOSSimulator: + switch SwiftBundler.host { + case .macOS: return .darwinApp case .linux: return .linuxGeneric @@ -37,12 +35,12 @@ enum BundlerChoice: String, CaseIterable { } /// A list of supported values for human consumption. - static var supportedHostValuesDescription: String { - let supportedChoices = allCases.filter { choice in - choice.isSupportedOnHostPlatform - } - return "(\(supportedChoices.map(\.rawValue).joined(separator: "|")))" - } + // static var supportedHostValuesDescription: String { + // let supportedChoices = allCases.filter { choice in + // choice.isSupportedOnHostPlatform + // } + // return "(\(supportedChoices.map(\.rawValue).joined(separator: "|")))" + // } /// Target platforms that the choice is valid for. var supportedTargetPlatforms: [Platform] { @@ -55,9 +53,9 @@ enum BundlerChoice: String, CaseIterable { } /// Host platforms that the choice is valid for. - var supportedHostPlatforms: [Platform] { - // Nice and simple one-to-one for now. With SwiftPM cross-compilation advancing - // I'm sure I'll eventually get cross-bundling working. - supportedTargetPlatforms - } + // var supportedHostPlatforms: [Platform] { + // // Nice and simple one-to-one for now. With SwiftPM cross-compilation advancing + // // I'm sure I'll eventually get cross-bundling working. + // supportedTargetPlatforms + // } } diff --git a/Sources/swift-bundler/SwiftBundler.swift b/Sources/swift-bundler/SwiftBundler.swift index bd06e4c5..f90caca4 100644 --- a/Sources/swift-bundler/SwiftBundler.swift +++ b/Sources/swift-bundler/SwiftBundler.swift @@ -8,7 +8,7 @@ struct SwiftBundler: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "swift-bundler", - abstract: "A tool for creating macOS apps from Swift packages.", + abstract: "A tool for creating apps from Swift packages.", version: "v" + version.description, shouldDisplay: true, subcommands: [ @@ -22,6 +22,14 @@ struct SwiftBundler: AsyncParsableCommand { GenerateXcodeSupportCommand.self, ListIdentitiesCommand.self, ] + .filter { + if Self.host != .macOS { + // Other non-macOS systems do not support running simulators and identities + $0 != SimulatorsCommand.self && $0 != ListIdentitiesCommand.self + } else { + true + } + } ) @Flag( @@ -35,4 +43,22 @@ struct SwiftBundler: AsyncParsableCommand { log.logLevel = .debug } } + + #if os(Linux) + static let host = Host.linux + #elseif os(macOS) + static let host = Host.macOS + #endif + + enum Host: Hashable, Codable { + case macOS + case linux + + var defaultForHostPlatform: Platform { + switch self { + case .macOS: .macOS + case .linux: .linux + } + } + } }