Repository: darrarski/xcframework-maker Branch: main Commit: f59c1c46f09a Files: 42 Total size: 55.4 KB Directory structure: gitextract_2cuer62l/ ├── .gitignore ├── .swiftpm/ │ └── xcode/ │ └── xcshareddata/ │ └── xcschemes/ │ ├── XCFrameworkMaker.xcscheme │ └── make-xcframework.xcscheme ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources/ │ ├── XCFrameworkMaker/ │ │ ├── Actions/ │ │ │ ├── AddArm64Simulator.swift │ │ │ ├── CopyFramework.swift │ │ │ ├── CopyPath.swift │ │ │ ├── CreateDir.swift │ │ │ ├── CreateTempDir.swift │ │ │ ├── CreateXCFramework.swift │ │ │ ├── DeletePath.swift │ │ │ ├── GetArchs.swift │ │ │ ├── LipoCreate.swift │ │ │ ├── LipoExtract.swift │ │ │ ├── LipoThin.swift │ │ │ ├── Log.swift │ │ │ ├── MakeXCFramework.swift │ │ │ └── RunShellCommand.swift │ │ └── Models/ │ │ ├── Arch.swift │ │ ├── LogLevel.swift │ │ └── Path.swift │ └── make-xcframework/ │ ├── MainCommand.swift │ └── main.swift └── Tests/ └── XCFrameworkMakerTests/ ├── Actions/ │ ├── AddArm64SimulatorTests.swift │ ├── CopyFrameworkTests.swift │ ├── CopyPathTests.swift │ ├── CreateDirTests.swift │ ├── CreateTempDirTests.swift │ ├── CreateXCFrameworkTests.swift │ ├── DeletePathTests.swift │ ├── GetArchsTests.swift │ ├── LipoCreateTests.swift │ ├── LipoExtractTests.swift │ ├── LipoThinTests.swift │ ├── LogTests.swift │ ├── MakeXCFrameworkTests.swift │ └── RunShellCommandTests.swift └── Models/ ├── LogLevelTests.swift └── PathTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/XCFrameworkMaker.xcscheme ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/make-xcframework.xcscheme ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Dariusz Rybicki Darrarski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.resolved ================================================ { "object": { "pins": [ { "package": "arm64-to-sim", "repositoryURL": "https://github.com/darrarski/arm64-to-sim.git", "state": { "branch": null, "revision": "37962d62d7f8d34875f793de510950112d860083", "version": "1.0.0" } }, { "package": "ShellOut", "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", "state": { "branch": null, "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", "version": "2.3.0" } }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, "revision": "986d191f94cec88f6350056da59c2e59e83d1229", "version": "0.4.3" } } ] }, "version": 1 } ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.4 import PackageDescription let package = Package( name: "xcframework-maker", platforms: [ .macOS(.v11), ], products: [ .library( name: "XCFrameworkMaker", targets: [ "XCFrameworkMaker", ] ), .executable( name: "make-xcframework", targets: [ "make-xcframework", ] ), ], dependencies: [ .package( name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "0.4.3") ), .package( name: "ShellOut", url: "https://github.com/JohnSundell/ShellOut.git", .upToNextMajor(from: "2.3.0") ), .package( name: "arm64-to-sim", url: "https://github.com/darrarski/arm64-to-sim.git", .upToNextMajor(from: "1.0.0") ), ], targets: [ .target( name: "XCFrameworkMaker", dependencies: [ .product( name: "ShellOut", package: "ShellOut" ), .product( name: "Arm64ToSim", package: "arm64-to-sim" ), ] ), .testTarget( name: "XCFrameworkMakerTests", dependencies: [ .target(name: "XCFrameworkMaker"), ] ), .executableTarget( name: "make-xcframework", dependencies: [ .target(name: "XCFrameworkMaker"), .product( name: "ArgumentParser", package: "swift-argument-parser" ), ] ), ] ) ================================================ FILE: README.md ================================================ # xcframework-maker ![swift 5.4](https://img.shields.io/badge/swift-5.4-orange.svg) ![platform macOS](https://img.shields.io/badge/platform-macOS-blue) ![SPM supported](https://img.shields.io/badge/SPM-supported-green) macOS utility for converting fat-frameworks to SPM-compatible XCFramework with arm64-simulator support. ## 📝 Description `make-xcframework` is a simple command-line utility written in Swift that creates **XCFramework** file from fat framework files. The resulting XCFramework file can be added as a dependency to your **Swift Package**, using `.binaryTarget` (read more in [official documentation](https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html)). Optionally, **arm64-simulator** support can be included in the resulting XCFramework to allow development on a computer with **Apple Silicon (M1)** processor without a need to run Xcode through Rosetta. The `xcframework-maker` Swift Package contains `make-xcframework` that can be used from the command line and `XCFrameworkMaker` library that you can integrate with your Swift Package and use programmatically. **Note:** [arm64-to-sim](http://github.com/darrarski/arm64-to-sim) is used to "hack" the native (device) **arm64** architecture slice so it can be used in a simulator running on Apple Silicon. This is an experimental feature, and **it can fail for many reasons**. It was tested and proved to be working with the `GoogleInteractiveMediaAds` dynamic fat framework, but your experience may vary. ## 🛠 Build Use Swift 5.4 for building the utility on macOS: ```sh swift build -c release ``` You can copy the executable or run it directly from the build directory: ```sh .build/release/make-xcframework ``` ## ▶️ Usage ``` OVERVIEW: Utility for creating XCFramework from legacy fat-framework files. Use this tool to create XCFramework from legacy fat-framework files. Resulting XCFramework can be added as a dependency to your Swift Package. Optionally arm64-simulator support can be included in the resulting XCFramework, so it can be used on M1 Mac without the need to run Xcode through Rosetta. USAGE: make-xcframework [-ios ] [-tvos ] [-arm64sim] -output [-verbose] OPTIONS: -ios iOS input framework path. Provide a path to the iOS fat framework that should be included in the resulting XCFramework. Eg "path/to/iOS/Framework.framework" -tvos tvOS input framework path. Provide a path to the tvOS fat framework that should be included in the resulting XCFramework. Eg "path/to/tvOS/Framework.framework" -arm64sim Add support for arm64 simulator. Use device-arm64 architecture slice as a simulator-arm64 architecture slice and include it the resulting XCFramework. This makes development possible on M1 Mac without using Rosetta. -output Output directory path. Provide a path to a directory where the resulting XCFramework should be created. Eg "path/to/output/directory" -verbose Log detailed info to standard output. When this flag is provided, detailed information about each performed action is logged to standard output. -help, -h Show help information. ``` ### Example - GoogleInteractiveMediaAds 1. Download GoogleInteractiveMediaAds fat-frameworks from Google website: - [IMA SDK for iOS](https://developers.google.com/interactive-media-ads/docs/sdks/ios/dai/download) - [IMA SDK for tvOS](https://developers.google.com/interactive-media-ads/docs/sdks/tvos/dai/download) 2. Unzip downloaded files. 3. Run `make-xcframework`: ```sh make-xcframework \ -ios path/to/ios/GoogleInteractiveMediaAds.framework \ -tvos path/to/tvos/GoogleInteractiveMediaAds.framework \ -arm64sim \ -output output/path ``` 4. Resulting XCFramework will be created in the provided output directory: ```sh output/path/GoogleInteractiveMediaAds.xcframework ``` ## ☕️ Do you like the project? Buy Me A Coffee ## 📄 License Copyright © 2021 Dariusz Rybicki Darrarski License: [MIT](LICENSE) ================================================ FILE: Sources/XCFrameworkMaker/Actions/AddArm64Simulator.swift ================================================ import Arm64ToSim /// Adds arm64 simulator support to a framework public struct AddArm64Simulator { var run: (Path, Path, Log?) throws -> Void /// Add arm64 simulator support to a framework /// - Parameters: /// - deviceFramework: Path to device framework file /// - simulatorFramework: Path to simulator framework file /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(deviceFramework: Path, simulatorFramework: Path, _ log: Log? = nil) throws { try run(deviceFramework, simulatorFramework, log) } } public extension AddArm64Simulator { static func live( lipoThin: LipoThin = .live(), lipoCrate: LipoCreate = .live(), arm64ToSim: @escaping (String) throws -> Void = arm64ToSim(_:), deletePath: DeletePath = .live() ) -> Self { .init { deviceFramework, simulatorFramework, log in log?(.normal, "[AddArm64Simulator]") log?(.verbose, "- deviceFramework: \(deviceFramework.string)") log?(.verbose, "- simulatorFramework: \(simulatorFramework.string)") let deviceBinary = deviceFramework.addingComponent(deviceFramework.filenameExcludingExtension) let simulatorBinary = simulatorFramework.addingComponent(simulatorFramework.filenameExcludingExtension) let arm64Binary = Path("\(simulatorBinary.string)-arm64") try lipoThin(input: deviceBinary, arch: .arm64, output: arm64Binary, log?.indented()) try arm64ToSim(arm64Binary.string) try lipoCrate(inputs: [simulatorBinary, arm64Binary], output: simulatorBinary, log?.indented()) try deletePath(arm64Binary, log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/CopyFramework.swift ================================================ /// Creates copy of a framework with provided architectures public struct CopyFramework { var run: (Path, [Arch], Path, Log?) throws -> Path /// Create copy of a framework with provided architectures /// - Parameters: /// - input: Path to the framework /// - archs: Architectures to be included in the copied framework /// - path: Path to a directory where framework copy will be created /// - log: Log action (defaults to nil for no logging) /// - Throws: Error /// - Returns: Path to the copied framework public func callAsFunction(_ input: Path, archs: [Arch], path: Path, _ log: Log? = nil) throws -> Path { try run(input, archs, path, log) } } public extension CopyFramework { static func live( createDir: CreateDir = .live(), copyPath: CopyPath = .live(), deletePath: DeletePath = .live(), lipoExtract: LipoExtract = .live() ) -> Self { .init { input, archs, path, log in log?(.normal, "[CopyFramework]") log?(.verbose, "- input: \(input.string)") log?(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))") log?(.verbose, "- path: \(path.string)") try createDir(path, log?.indented()) let output = path.addingComponent(input.lastComponent) try copyPath(of: input, at: output, log?.indented()) let outputBinary = output.addingComponent(output.filenameExcludingExtension) try deletePath(outputBinary, log?.indented()) let inputBinary = input.addingComponent(input.filenameExcludingExtension) try lipoExtract.callAsFunction(input: inputBinary, archs: archs, output: outputBinary, log?.indented()) log?(.verbose, "- output: \(output.string)") return output } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/CopyPath.swift ================================================ /// Creates copy of file or directory public struct CopyPath { var run: (Path, Path, Log?) throws -> Void /// Create copy of file or directory /// - Parameters: /// - source: Source file or directory path /// - destination: Destination path /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(of source: Path, at destination: Path, _ log: Log? = nil) throws { try run(source, destination, log) } } public extension CopyPath { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { source, destination, log in log?(.normal, "[CopyPath]") log?(.verbose, "- source: \(source.string)") log?(.verbose, "- destination: \(destination.string)") _ = try runShellCommand("cp -fR \(source.string) \(destination.string)", log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/CreateDir.swift ================================================ /// Creates directory public struct CreateDir { var run: (Path, Log?) throws -> Void /// Create directory at provied path /// - Parameters: /// - path: Path of the directory to create /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(_ path: Path, _ log: Log? = nil) throws { try run(path, log) } } public extension CreateDir { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { path, log in log?(.normal, "[CreateDir]") log?(.verbose, "- path: \(path.string)") _ = try runShellCommand("mkdir -p \(path.string)", log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/CreateTempDir.swift ================================================ import Foundation /// Creates new temporary directory public struct CreateTempDir { var run: (Log?) throws -> Path /// Create new temporary directory and return its path /// - Parameter log: Log action (defaults to nil for no logging) /// - Throws: Error /// - Returns: Path to new temporary directory public func callAsFunction(_ log: Log? = nil) throws -> Path { try run(log) } } public extension CreateTempDir { static func live( basePath: String = FileManager.default.temporaryDirectory.path, randomString: @escaping () -> String = { UUID().uuidString }, createDir: CreateDir = .live() ) -> Self { .init { log in log?(.normal, "[CreateTempDir]") let path = Path(basePath).addingComponent("XCFrameworkMaker_\(randomString())") try createDir(path, log?.indented()) log?(.verbose, "- path: \(path.string)") return path } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/CreateXCFramework.swift ================================================ /// Creates XCFramework from provided thin frameworks public struct CreateXCFramework { var run: ([Path], Path, Log?) throws -> Void /// Create XCFramework from provided thin frameworks /// - Parameters: /// - frameworks: Paths to thin frameworks /// - path: Path to a directory where resulting XCFramework will be created /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(from frameworks: [Path], at path: Path, _ log: Log? = nil) throws { try run(frameworks, path, log) } } public extension CreateXCFramework { static func live( deletePath: DeletePath = .live(), runShellCommand: RunShellCommand = .live() ) -> Self { .init { frameworks, path, log in log?(.normal, "[CreateXCFramework]") log?(.verbose, "- frameworks: \n\t\(frameworks.map(\.string).joined(separator: "\n\t"))") log?(.verbose, "- path: \(path.string)") let frameworkOptions = frameworks.map { "-framework \($0.string)" }.joined(separator: " ") let frameworkName = frameworks.first?.filenameExcludingExtension ?? "" let output = path.addingComponent("\(frameworkName).xcframework") try deletePath(output, log?.indented()) _ = try runShellCommand( "xcodebuild -create-xcframework \(frameworkOptions) -output \(output.string)", log?.indented() ) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/DeletePath.swift ================================================ /// Deletes file or directory public struct DeletePath { var run: (Path, Log?) throws -> Void /// Delete file or directory /// - Parameters: /// - path: Path to file or directory to be removed /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(_ path: Path, _ log: Log? = nil) throws { try run(path, log) } } public extension DeletePath { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { path, log in log?(.normal, "[DeletePath]") log?(.verbose, "- path: \(path.string)") _ = try runShellCommand("rm -Rf \(path.string)", log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/GetArchs.swift ================================================ /// Gets architectures contained in a framework public struct GetArchs { var run: (Path, Log?) throws -> [Arch] /// Get architectures contained in framework /// - Parameters: /// - frameworkPath: Path to framework /// - log: Log action (defaults to nil for no logging) /// - Throws: Error /// - Returns: Architectures contained in the framework public func callAsFunction(inFramework frameworkPath: Path, _ log: Log? = nil) throws -> [Arch] { try run(frameworkPath, log) } } public extension GetArchs { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { frameworkPath, log in log?(.normal, "[GetArchs]") log?(.verbose, "- frameworkPath: \(frameworkPath.string)") let frameworkName = frameworkPath.filenameExcludingExtension let binaryPath = frameworkPath.addingComponent(frameworkName) let archs = try runShellCommand("lipo \(binaryPath.string) -archs", log?.indented()) .components(separatedBy: " ") .compactMap(Arch.init(rawValue:)) log?(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))") return archs } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/LipoCreate.swift ================================================ /// Use lipo to create binary file from other binary files public struct LipoCreate { var run: ([Path], Path, Log?) throws -> Void /// Create binary file from other binary files /// - Parameters: /// - inputs: Paths to input files /// - output: Path to output file /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(inputs: [Path], output: Path, _ log: Log? = nil) throws { try run(inputs, output, log) } } public extension LipoCreate { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { inputs, output, log in log?(.normal, "[LipoCreate]") log?(.verbose, "- inputs: \n\t\(inputs.map(\.string).joined(separator: "\n\t"))") log?(.verbose, "- output: \(output.string)") let input = inputs.map(\.string).joined(separator: " ") _ = try runShellCommand("lipo \(input) -create -output \(output.string)", log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/LipoExtract.swift ================================================ /// Use lipo to extract provided architectures from binary file public struct LipoExtract { var run: (Path, [Arch], Path, Log?) throws -> Void /// Extract provided architectures from binary file /// - Parameters: /// - input: Path to the binary file /// - archs: Architectures to be extracted /// - output: Path to the extracted binary file /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(input: Path, archs: [Arch], output: Path, _ log: Log? = nil) throws { try run(input, archs, output, log) } } public extension LipoExtract { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { input, archs, output, log in log?(.normal, "[LipoExtract]") log?(.verbose, "- input: \(input.string)") log?(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))") log?(.verbose, "- output: \(input.string)") let extract = archs.map { "-extract \($0)" }.joined(separator: " ") _ = try runShellCommand("lipo \(input.string) \(extract) -output \(output.string)", log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/LipoThin.swift ================================================ /// Use lipo to create thin binary file with provided architecture public struct LipoThin { var run: (Path, Arch, Path, Log?) throws -> Void /// Create thin binary file with provided architecture /// - Parameters: /// - input: Path to the input file /// - arch: Architecture to be included in the output /// - output: Path to the output file /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction(input: Path, arch: Arch, output: Path, _ log: Log? = nil) throws { try run(input, arch, output, log) } } public extension LipoThin { static func live( runShellCommand: RunShellCommand = .live() ) -> Self { .init { input, arch, output, log in log?(.normal, "[LipoThin]") log?(.verbose, "- input: \(input.string)") log?(.verbose, "- arch: \(arch.rawValue)") log?(.verbose, "- output: \(output.string)") _ = try runShellCommand( "lipo \(input.string) -thin \(arch.rawValue) -output \(output.string)", log?.indented() ) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/Log.swift ================================================ /// Logs messages public struct Log { var run: (LogLevel, String) -> Void /// Log message /// - Parameters: /// - level: Log level /// - message: Message to be logged public func callAsFunction(_ level: LogLevel, _ message: String) { run(level, message) } } public extension Log { static func live( level logginLevel: LogLevel = .normal, print: @escaping (String) -> Void = { print($0) } ) -> Self { .init { level, message in guard level <= logginLevel else { return } print(message) } } } public extension Log { /// Returns modified Log that indents messages with a tab character /// - Returns: Log func indented() -> Self { .init { level, message in let indentation = "\t" let indentedMessage = message .split(separator: "\n") .map { indentation + $0 } .joined(separator: "\n") self.callAsFunction(level, indentedMessage) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/MakeXCFramework.swift ================================================ import Foundation /// Creates XCFramework from provided fat frameworks public struct MakeXCFramework { var run: (Path?, Path?, Bool, Path, Log?) throws -> Void /// Create XCFramework from provided fat frameworks /// - Parameters: /// - iOSPath: Path to iOS fat framework /// - tvOSPath: Path to tvOS fat framework /// - arm64sim: If true, add arm64-simulator support /// - path: Path to a directory where resulting XCFramework will be created /// - log: Log action (defaults to nil for no logging) /// - Throws: Error public func callAsFunction( iOS iOSPath: Path?, tvOS tvOSPath: Path?, arm64sim: Bool, at path: Path, _ log: Log? = nil ) throws { try run(iOSPath, tvOSPath, arm64sim, path, log) } } extension MakeXCFramework { public struct EmptyInputError: LocalizedError, Equatable { public init() {} public var errorDescription: String? { "Empty inputs. Provide at least one input fat framework." } } } public extension MakeXCFramework { static func live( createTempDir: CreateTempDir = .live(), getArchs: GetArchs = .live(), copyFramework: CopyFramework = .live(), addArm64Simulator: AddArm64Simulator = .live(), createXCFramework: CreateXCFramework = .live() ) -> Self { .init { iOSPath, tvOSPath, arm64sim, output, log in log?(.normal, "[MakeXCFramework]") log?(.verbose, "- iOSPath: \(iOSPath?.string ?? "nil")") log?(.verbose, "- tvOSPath: \(tvOSPath?.string ?? "nil")") log?(.verbose, "- arm64sim: \(arm64sim)") log?(.verbose, "- output: \(output.string)") guard iOSPath != nil || tvOSPath != nil else { throw EmptyInputError() } let tempDir = try createTempDir(log?.indented()) var thinFrameworks = [Path]() if let path = iOSPath { let archs = try getArchs(inFramework: path, log?.indented()) let deviceArchs = [Arch.armv7, .arm64].filter(archs.contains(_:)) let simulatorArchs = [Arch.i386, .x86_64].filter(archs.contains(_:)) let deviceOutput = tempDir.addingComponent("ios-device") let simulatorOutput = tempDir.addingComponent("ios-simulator") let deviceFramework = try copyFramework(path, archs: deviceArchs, path: deviceOutput, log?.indented()) let simulatorFramework = try copyFramework(path, archs: simulatorArchs, path: simulatorOutput, log?.indented()) if arm64sim { try addArm64Simulator( deviceFramework: deviceFramework, simulatorFramework: simulatorFramework, log?.indented() ) } thinFrameworks.append(contentsOf: [deviceFramework, simulatorFramework]) } if let path = tvOSPath { let archs = try getArchs(inFramework: path, log?.indented()) let deviceArchs = [Arch.armv7, .arm64].filter(archs.contains(_:)) let simulatorArchs = [Arch.i386, .x86_64].filter(archs.contains(_:)) let deviceOutput = tempDir.addingComponent("tvos-device") let simulatorOutput = tempDir.addingComponent("tvos-simulator") let deviceFramework = try copyFramework(path, archs: deviceArchs, path: deviceOutput, log?.indented()) let simulatorFramework = try copyFramework(path, archs: simulatorArchs, path: simulatorOutput, log?.indented()) if arm64sim { try addArm64Simulator( deviceFramework: deviceFramework, simulatorFramework: simulatorFramework, log?.indented() ) } thinFrameworks.append(contentsOf: [deviceFramework, simulatorFramework]) } try createXCFramework(from: thinFrameworks, at: output, log?.indented()) } } } ================================================ FILE: Sources/XCFrameworkMaker/Actions/RunShellCommand.swift ================================================ import ShellOut /// Execute shell command public struct RunShellCommand { var run: (String, Log?) throws -> String /// Execure shell command /// - Parameters: /// - command: Command to execute /// - log: Log action (defaults to nil for no logging) /// - Throws: Error /// - Returns: Shell command output public func callAsFunction(_ command: String, _ log: Log? = nil) throws -> String { try run(command, log) } } public extension RunShellCommand { static func live( shellOut: @escaping (String) throws -> String = { try shellOut(to: $0) } ) -> Self { .init { command, log in log?(.normal, "[RunShellCommand]") log?(.verbose, "- command: \(command)") let output = try shellOut(command) log?(.verbose, "- output: \(output)") return output } } } ================================================ FILE: Sources/XCFrameworkMaker/Models/Arch.swift ================================================ public enum Arch: String { case i386 case x86_64 case armv7 case arm64 } ================================================ FILE: Sources/XCFrameworkMaker/Models/LogLevel.swift ================================================ public enum LogLevel: Equatable, CaseIterable { case normal case verbose } extension LogLevel: Comparable { public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { allCases.firstIndex(of: lhs)! < allCases.firstIndex(of: rhs)! } } ================================================ FILE: Sources/XCFrameworkMaker/Models/Path.swift ================================================ import Foundation public struct Path: Equatable, Hashable { public init(_ string: String) { self.string = string } var string: String } extension Path { var lastComponent: String { string.split(separator: "/").last.map(String.init) ?? "" } var fileExtension: String? { let lastComponent = self.lastComponent guard lastComponent.contains(".") else { return nil } return lastComponent.split(separator: ".").last.map(String.init) } var filenameExcludingExtension: String { let lastComponent = self.lastComponent guard lastComponent.contains(".") else { return lastComponent } var filenameComponents = lastComponent.split(separator: ".").map(String.init) guard filenameComponents.count > 1 else { return lastComponent } filenameComponents.removeLast() return filenameComponents.joined(separator: ".") } func addingComponent(_ component: String) -> Path { var newString = string if newString.hasSuffix("/") == false { newString.append("/") } newString.append(component) return Path(newString) } } ================================================ FILE: Sources/make-xcframework/MainCommand.swift ================================================ import ArgumentParser import Foundation import XCFrameworkMaker struct MainCommand: ParsableCommand { static var makeXCFramework: MakeXCFramework = .live() // MARK: - ParsableCommand static let configuration = CommandConfiguration( commandName: "make-xcframework", abstract: "Utility for creating XCFramework from legacy fat-framework files.", discussion: "Use this tool to create XCFramework from legacy fat-framework files. Resulting XCFramework can be added as a dependency to your Swift Package. Optionally arm64-simulator support can be included in the resulting XCFramework, so it can be used on M1 Mac without the need to run Xcode through Rosetta.", helpNames: [.short, .customLong("help", withSingleDash: true)] ) struct InputOptions: ParsableArguments { @Option( name: .customLong("ios", withSingleDash: true), help: ArgumentHelp( "iOS input framework path.", discussion: "Provide a path to the iOS fat framework that should be included in the resulting XCFramework. Eg \"path/to/iOS/Framework.framework\"", valueName: "path" ), completion: .file(extensions: ["framework"]) ) var ios: String? @Option( name: .customLong("tvos", withSingleDash: true), help: ArgumentHelp( "tvOS input framework path.", discussion: "Provide a path to the tvOS fat framework that should be included in the resulting XCFramework. Eg \"path/to/tvOS/Framework.framework\"", valueName: "path" ), completion: .file(extensions: ["framework"]) ) var tvos: String? func validate() throws { guard ios != nil || tvos != nil else { throw MakeXCFramework.EmptyInputError() } } } @OptionGroup var inputs: InputOptions @Flag( name: .customLong("arm64sim", withSingleDash: true), help: ArgumentHelp( "Add support for arm64 simulator.", discussion: "Use device-arm64 architecture slice as a simulator-arm64 architecture slice and include it the resulting XCFramework. This makes development possible on M1 Mac without using Rosetta." ) ) var arm64sim: Bool = false @Option( name: .customLong("output", withSingleDash: true), help: ArgumentHelp( "Output directory path.", discussion: "Provide a path to a directory where the resulting XCFramework should be created. Eg \"path/to/output/directory\"", valueName: "path" ), completion: .directory ) var output: String @Flag( name: .customLong("verbose", withSingleDash: true), help: ArgumentHelp( "Log detailed info to standard output.", discussion: "When this flag is provided, detailed information about each performed action is logged to standard output." ) ) var verbose: Bool = false func run() throws { try Self.makeXCFramework( iOS: inputs.ios.map(Path.init(_:)), tvOS: inputs.tvos.map(Path.init(_:)), arm64sim: arm64sim, at: Path(output), verbose ? Log.live(level: .verbose) : nil ) } } ================================================ FILE: Sources/make-xcframework/main.swift ================================================ MainCommand.main() ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/AddArm64SimulatorTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class AddArm64SimulatorTests: XCTestCase { enum Action: Equatable { case didLipoThin(Path, Arch, Path) case didLipoCreate([Path], Path) case didArm64ToSim(String) case didDeletePath(Path) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = AddArm64Simulator.live( lipoThin: .init { input, arch, output, _ in didPerformActions.append(.didLipoThin(input, arch, output)) }, lipoCrate: .init { input, output, _ in didPerformActions.append(.didLipoCreate(input, output)) }, arm64ToSim: { path in didPerformActions.append(.didArm64ToSim(path)) }, deletePath: .init { path, _ in didPerformActions.append(.didDeletePath(path)) } ) let deviceFramework = Path("device/Framework.framework") let simulatorFramework = Path("simulator/Framework.framework") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(deviceFramework: deviceFramework, simulatorFramework: simulatorFramework, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[AddArm64Simulator]"), .didLog(.verbose, "- deviceFramework: \(deviceFramework.string)"), .didLog(.verbose, "- simulatorFramework: \(simulatorFramework.string)"), .didLipoThin(Path("device/Framework.framework/Framework"), .arm64, Path("simulator/Framework.framework/Framework-arm64")), .didArm64ToSim("simulator/Framework.framework/Framework-arm64"), .didLipoCreate([Path("simulator/Framework.framework/Framework"), Path("simulator/Framework.framework/Framework-arm64")], Path("simulator/Framework.framework/Framework")), .didDeletePath(Path("simulator/Framework.framework/Framework-arm64")) ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/CopyFrameworkTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class CopyFrameworkTests: XCTestCase { enum Action: Equatable { case didCreateDir(Path) case didCopyPath(Path, Path) case didDeletePath(Path) case didLipoExtract(Path, [Arch], Path) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = CopyFramework.live( createDir: .init { path, _ in didPerformActions.append(.didCreateDir(path)) }, copyPath: .init { source, destination, _ in didPerformActions.append(.didCopyPath(source, destination)) }, deletePath: .init { path, _ in didPerformActions.append(.didDeletePath(path)) }, lipoExtract: .init { input, archs, output, _ in didPerformActions.append(.didLipoExtract(input, archs, output)) } ) let input = Path("input/Framework.framework") let archs = [Arch.i386, .arm64] let path = Path("output/path") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } let output = try sut(input, archs: archs, path: path, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[CopyFramework]"), .didLog(.verbose, "- input: \(input.string)"), .didLog(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))"), .didLog(.verbose, "- path: \(path.string)"), .didCreateDir(Path("output/path")), .didCopyPath(Path("input/Framework.framework"), Path("output/path/Framework.framework")), .didDeletePath(Path("output/path/Framework.framework/Framework")), .didLipoExtract(Path("input/Framework.framework/Framework"), [.i386, .arm64], Path("output/path/Framework.framework/Framework")), .didLog(.verbose, "- output: \(output.string)") ]) XCTAssertEqual(output, path.addingComponent(input.lastComponent)) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/CopyPathTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class CopyPathTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = CopyPath.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let source = Path("source") let destination = Path("destination") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(of: source, at: destination, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[CopyPath]"), .didLog(.verbose, "- source: \(source.string)"), .didLog(.verbose, "- destination: \(destination.string)"), .didRunShellCommand("cp -fR source destination") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/CreateDirTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class CreateDirTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = CreateDir.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let path = Path("new/directory/path") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(path, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[CreateDir]"), .didLog(.verbose, "- path: \(path.string)"), .didRunShellCommand("mkdir -p new/directory/path") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/CreateTempDirTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class CreateTempDirTests: XCTestCase { enum Action: Equatable { case didCreateDir(Path) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let basePath = "temp/dir/base/path" let randomString = "random" let sut = CreateTempDir.live( basePath: basePath, randomString: { randomString }, createDir: .init { path, _ in didPerformActions.append(.didCreateDir(path)) } ) let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } let path = try sut(log) let expectedPath = Path(basePath).addingComponent("XCFrameworkMaker_\(randomString)") XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[CreateTempDir]"), .didCreateDir(expectedPath), .didLog(.verbose, "- path: \(path.string)") ]) XCTAssertEqual(path, expectedPath) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/CreateXCFrameworkTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class CreateXCFrameworkTests: XCTestCase { enum Action: Equatable { case didDeletePath(Path) case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = CreateXCFramework.live( deletePath: .init { path, _ in didPerformActions.append(.didDeletePath(path)) }, runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" } ) let frameworks = [ Path("device/Framework.framework"), Path("simulator/Framework.framework") ] let path = Path("output/path") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(from: frameworks, at: path, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[CreateXCFramework]"), .didLog(.verbose, "- frameworks: \n\t\(frameworks.map(\.string).joined(separator: "\n\t"))"), .didLog(.verbose, "- path: \(path.string)"), .didDeletePath(Path("output/path/Framework.xcframework")), .didRunShellCommand("xcodebuild -create-xcframework -framework device/Framework.framework -framework simulator/Framework.framework -output output/path/Framework.xcframework") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/DeletePathTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class DeletePathTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = DeletePath.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let path = Path("some/path") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(path, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[DeletePath]"), .didLog(.verbose, "- path: \(path.string)"), .didRunShellCommand("rm -Rf some/path") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/GetArchsTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class GetArchsTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = GetArchs.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "i386 arm64 unknown" }) let frameworkPath = Path("path/to/Some.framework") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } let archs = try sut(inFramework: frameworkPath, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[GetArchs]"), .didLog(.verbose, "- frameworkPath: \(frameworkPath.string)"), .didRunShellCommand("lipo path/to/Some.framework/Some -archs"), .didLog(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))"), ]) XCTAssertEqual(archs, [.i386, .arm64]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/LipoCreateTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class LipoCreateTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = LipoCreate.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let inputs = [Path("input/file1"), Path("input/file2")] let output = Path("output/file") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(inputs: inputs, output: output, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[LipoCreate]"), .didLog(.verbose, "- inputs: \n\t\(inputs.map(\.string).joined(separator: "\n\t"))"), .didLog(.verbose, "- output: \(output.string)"), .didRunShellCommand("lipo input/file1 input/file2 -create -output output/file") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/LipoExtractTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class LipoExtractTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = LipoExtract.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let input = Path("input/file") let archs = [Arch.i386, .arm64] let output = Path("output/file") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(input: input, archs: archs, output: output, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[LipoExtract]"), .didLog(.verbose, "- input: \(input.string)"), .didLog(.verbose, "- archs: \(archs.map(\.rawValue).joined(separator: ", "))"), .didLog(.verbose, "- output: \(input.string)"), .didRunShellCommand("lipo input/file -extract i386 -extract arm64 -output output/file") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/LipoThinTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class LipoThinTests: XCTestCase { enum Action: Equatable { case didRunShellCommand(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let sut = LipoThin.live(runShellCommand: .init { command, _ in didPerformActions.append(.didRunShellCommand(command)) return "" }) let input = Path("input/file") let arch = Arch.arm64 let output = Path("output/file") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut(input: input, arch: arch, output: output, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[LipoThin]"), .didLog(.verbose, "- input: \(input.string)"), .didLog(.verbose, "- arch: \(arch.rawValue)"), .didLog(.verbose, "- output: \(output.string)"), .didRunShellCommand("lipo input/file -thin arm64 -output output/file") ]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/LogTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class LogTests: XCTestCase { func testNormalLogging() { var logged = [String]() let sut = Log.live( level: .normal, print: { logged.append($0) } ) sut(.normal, "Normal level log") sut(.verbose, "Verbose level log") XCTAssertEqual(logged, ["Normal level log"]) } func testVerboseLogging() { var logged = [String]() let sut = Log.live( level: .verbose, print: { logged.append($0) } ) sut(.normal, "Normal level log") sut(.verbose, "Verbose level log") XCTAssertEqual(logged, [ "Normal level log", "Verbose level log" ]) } func testIndentedLogging() { var logged = [String]() let sut = Log.live( level: .normal, print: { logged.append($0) } ).indented() sut(.normal, "multiline\nlog\nmessage") XCTAssertEqual(logged, ["\tmultiline\n\tlog\n\tmessage"]) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/MakeXCFrameworkTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class MakeXCFrameworkTests: XCTestCase { enum Action: Equatable { case didCreateTempDir case didGetArchs(Path) case didCopyFramework(Path, [Arch], Path) case didAddArm64Simulator(Path, Path) case didCreateXCFramework([Path], Path) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let iOSPath = Path("ios/Framework.framework") let tvOSPath = Path("tvos/Framework.framework") let createdTempDir = Path("temp/path") let archs: [Path: [Arch]] = [ iOSPath: [.i386, .x86_64, .armv7, .arm64], tvOSPath: [.x86_64, .arm64] ] let copiedFrameworks: [Path: Path] = [ iOSPath: Path("copy/ios/Framework.framework"), tvOSPath: Path("copy/tvos/Framework.framework") ] let sut = MakeXCFramework.live( createTempDir: .init { _ in didPerformActions.append(.didCreateTempDir) return createdTempDir }, getArchs: .init { path, _ in didPerformActions.append(.didGetArchs(path)) return archs[path]! }, copyFramework: .init { input, archs, path, _ in didPerformActions.append(.didCopyFramework(input, archs, path)) return copiedFrameworks[input]! }, addArm64Simulator: .init { device, simulator, _ in didPerformActions.append(.didAddArm64Simulator(device, simulator)) }, createXCFramework: .init { frameworks, path, _ in didPerformActions.append(.didCreateXCFramework(frameworks, path)) } ) let output = Path("output/path") let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } try sut.callAsFunction(iOS: iOSPath, tvOS: tvOSPath, arm64sim: true, at: output, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[MakeXCFramework]"), .didLog(.verbose, "- iOSPath: \(iOSPath.string)"), .didLog(.verbose, "- tvOSPath: \(tvOSPath.string)"), .didLog(.verbose, "- arm64sim: true"), .didLog(.verbose, "- output: \(output.string)"), .didCreateTempDir, .didGetArchs(iOSPath), .didCopyFramework(iOSPath, [.armv7, .arm64], createdTempDir.addingComponent("ios-device")), .didCopyFramework(iOSPath, [.i386, .x86_64], createdTempDir.addingComponent("ios-simulator")), .didAddArm64Simulator(copiedFrameworks[iOSPath]!, copiedFrameworks[iOSPath]!), .didGetArchs(tvOSPath), .didCopyFramework(tvOSPath, [.arm64], createdTempDir.addingComponent("tvos-device")), .didCopyFramework(tvOSPath, [.x86_64], createdTempDir.addingComponent("tvos-simulator")), .didAddArm64Simulator(copiedFrameworks[tvOSPath]!, copiedFrameworks[tvOSPath]!), .didCreateXCFramework([ copiedFrameworks[iOSPath]!, copiedFrameworks[iOSPath]!, copiedFrameworks[tvOSPath]!, copiedFrameworks[tvOSPath]! ], output) ]) } func testEmptyInputFailure() { let sut = MakeXCFramework.live( createTempDir: .init { _ in fatalError() }, getArchs: .init { _, _ in fatalError() }, copyFramework: .init { _, _, _, _ in fatalError() }, createXCFramework: .init { _, _, _ in fatalError() } ) var catchedError: Error? do { try sut(iOS: nil, tvOS: nil, arm64sim: true, at: Path("")) } catch { catchedError = error } XCTAssertEqual(catchedError as? MakeXCFramework.EmptyInputError, MakeXCFramework.EmptyInputError()) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Actions/RunShellCommandTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class RunShellCommandTests: XCTestCase { enum Action: Equatable { case didShellOut(String) case didLog(LogLevel, String) } func testHappyPath() throws { var didPerformActions = [Action]() let shellOutput = "shell output" let sut = RunShellCommand.live(shellOut: { command in didPerformActions.append(.didShellOut(command)) return shellOutput }) let shellCommand = "shell command" let log = Log { level, message in didPerformActions.append(.didLog(level, message)) } let result = try sut(shellCommand, log) XCTAssertEqual(didPerformActions, [ .didLog(.normal, "[RunShellCommand]"), .didLog(.verbose, "- command: \(shellCommand)"), .didShellOut(shellCommand), .didLog(.verbose, "- output: \(shellOutput)") ]) XCTAssertEqual(result, shellOutput) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Models/LogLevelTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class LogLevelTests: XCTestCase { func testComparable() { XCTAssertTrue(LogLevel.normal < LogLevel.verbose) XCTAssertTrue(LogLevel.normal <= LogLevel.verbose) XCTAssertFalse(LogLevel.normal > LogLevel.verbose) XCTAssertFalse(LogLevel.normal >= LogLevel.verbose) } } ================================================ FILE: Tests/XCFrameworkMakerTests/Models/PathTests.swift ================================================ import XCTest @testable import XCFrameworkMaker final class PathTests: XCTestCase { func testAddingComponent() { XCTAssertEqual(Path("some/path").addingComponent("component").string, "some/path/component") XCTAssertEqual(Path("some/path/").addingComponent("component").string, "some/path/component") } func testLastComponent() { XCTAssertEqual(Path("some/file").lastComponent, "file") XCTAssertEqual(Path("some/file.extension").lastComponent, "file.extension") XCTAssertEqual(Path("some/directory/").lastComponent, "directory") XCTAssertEqual(Path("path").lastComponent, "path") XCTAssertEqual(Path("").lastComponent, "") XCTAssertEqual(Path("/").lastComponent, "") XCTAssertEqual(Path("//").lastComponent, "") XCTAssertEqual(Path("path/").lastComponent, "path") XCTAssertEqual(Path("path//").lastComponent, "path") XCTAssertEqual(Path("/path/").lastComponent, "path") XCTAssertEqual(Path("some/path///").lastComponent, "path") } func testFileExtension() { XCTAssertNil(Path("file").fileExtension) XCTAssertNil(Path("path/to/some/file").fileExtension) XCTAssertEqual(Path("file.ext").fileExtension, "ext") XCTAssertEqual(Path("file.name.ext").fileExtension, "ext") XCTAssertEqual(Path("path/to/file.ext").fileExtension, "ext") XCTAssertEqual(Path("/path/to/file.ext").fileExtension, "ext") XCTAssertEqual(Path("path/to/file.ext/").fileExtension, "ext") XCTAssertEqual(Path("/path/to/file.ext//").fileExtension, "ext") } func testFilenameExcludingExtension() { XCTAssertEqual(Path("file").filenameExcludingExtension, "file") XCTAssertEqual(Path("file.ext").filenameExcludingExtension, "file") XCTAssertEqual(Path("file.name.ext").filenameExcludingExtension, "file.name") XCTAssertEqual(Path("path/to/file.ext").filenameExcludingExtension, "file") XCTAssertEqual(Path("/path/to/file.ext").filenameExcludingExtension, "file") XCTAssertEqual(Path("path/to/file.ext/").filenameExcludingExtension, "file") XCTAssertEqual(Path("/path/to/file.ext//").filenameExcludingExtension, "file") } }