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



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?
## 📄 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")
}
}