├── .gitignore ├── LICENSE.md ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── XCFrameworkKit │ ├── Version.swift │ ├── XCFrameworkAssembler.swift │ └── XCFrameworkBuilder.swift └── xcframework │ ├── Commands │ ├── AssembleCommand.swift │ ├── BuildCommand.swift │ └── VersionCommand.swift │ └── main.swift └── Tests ├── LinuxMain.swift ├── XCFrameworkKitTests └── test.swift └── xcframeworkTests ├── XCTestManifests.swift └── xcframeworkTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .xcframework 6 | Carthage/ 7 | XCFrameworkKit.framework.zip 8 | xcframework.pkg 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jeff Lett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/xcrun make -f 2 | 3 | TOOL_TEMPORARY_FOLDER?=/tmp/xcframework.dst 4 | PREFIX?=/usr/local 5 | 6 | OUTPUT_PACKAGE=xcframework.pkg 7 | FRAMEWORK_NAME=XCFrameworkKit 8 | 9 | TOOL_EXECUTABLE=./.build/release/xcframework 10 | BINARIES_FOLDER=/usr/local/bin 11 | 12 | # ZSH_COMMAND · run single command in `zsh` shell, ignoring most `zsh` startup files. 13 | ZSH_COMMAND := ZDOTDIR='/var/empty' zsh -o NO_GLOBAL_RCS -c 14 | # RM_SAFELY · `rm -rf` ensuring first and only parameter is non-null, contains more than whitespace, non-root if resolving absolutely. 15 | RM_SAFELY := $(ZSH_COMMAND) '[[ ! $${1:?} =~ "^[[:space:]]+\$$" ]] && [[ $${1:A} != "/" ]] && [[ $${\#} == "1" ]] && noglob rm -rf $${1:A}' -- 16 | 17 | VERSION_STRING=$(shell git describe --abbrev=0 --tags) 18 | 19 | RM=rm -f 20 | MKDIR=mkdir -p 21 | SUDO=sudo 22 | CP=cp 23 | 24 | .PHONY: all clean test installables package install uninstall xcodeproj xcodetest archive release 25 | 26 | all: installables 27 | 28 | clean: 29 | swift package clean 30 | 31 | test: 32 | swift test 33 | 34 | installables: 35 | swift build -c release 36 | 37 | package: installables archive 38 | $(MKDIR) "$(TOOL_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" 39 | $(CP) "$(TOOL_EXECUTABLE)" "$(TOOL_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" 40 | 41 | pkgbuild \ 42 | --identifier "com.jefflett.xcframework" \ 43 | --install-location "/" \ 44 | --root "$(TOOL_TEMPORARY_FOLDER)" \ 45 | --version "$(VERSION_STRING)" \ 46 | "$(OUTPUT_PACKAGE)" 47 | 48 | install: installables 49 | $(SUDO) $(CP) -f "$(TOOL_EXECUTABLE)" "$(BINARIES_FOLDER)" 50 | 51 | uninstall: 52 | $(RM) "$(BINARIES_FOLDER)/xcframework" 53 | 54 | xcodeproj: 55 | swift package generate-xcodeproj 56 | 57 | xcodetest: xcodeproj 58 | xcodebuild -scheme xcframework build test 59 | 60 | codecoverage: xcodeproj 61 | xcodebuild -scheme xcframework -enableCodeCoverage YES build test -quiet 62 | 63 | archive: 64 | carthage build --no-skip-current --platform mac 65 | carthage archive $(FRAMEWORK_NAME) 66 | 67 | release: | test xcodetest archive package install 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Commandant", 6 | "repositoryURL": "https://github.com/Carthage/Commandant", 7 | "state": { 8 | "branch": null, 9 | "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4", 10 | "version": "0.17.0" 11 | } 12 | }, 13 | { 14 | "package": "Nimble", 15 | "repositoryURL": "https://github.com/Quick/Nimble.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "43304bf2b1579fd555f2fdd51742771c1e4f2b98", 19 | "version": "8.0.1" 20 | } 21 | }, 22 | { 23 | "package": "Quick", 24 | "repositoryURL": "https://github.com/Quick/Quick.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "94df9b449508344667e5afc7e80f8bcbff1e4c37", 28 | "version": "2.1.0" 29 | } 30 | }, 31 | { 32 | "package": "Shell", 33 | "repositoryURL": "https://github.com/AlwaysRightInstitute/Shell", 34 | "state": { 35 | "branch": null, 36 | "revision": "9982634c51b506d42fcb11b1c85e7ccc145679bd", 37 | "version": "0.1.4" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "xcframework", 6 | products: [ 7 | .library(name: "XCFrameworkKit", targets: ["XCFrameworkKit"]), 8 | .executable(name: "xcframework", targets: ["xcframework"]) 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/AlwaysRightInstitute/Shell", from: "0.1.4"), 12 | .package(url: "https://github.com/Carthage/Commandant", from: "0.17.0") 13 | ], 14 | targets: [ 15 | .target( 16 | name: "xcframework", 17 | dependencies: ["XCFrameworkKit", "Commandant"]), 18 | .target( 19 | name: "XCFrameworkKit", 20 | dependencies: ["Shell"]), 21 | .testTarget( 22 | name: "XCFrameworkKitTests", 23 | dependencies: ["XCFrameworkKit"]), 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xcframework 2 | 3 | [![Swift Version](https://img.shields.io/badge/Swift-5.1-orange.svg?style=for-the-badge)](https://swift.org) 4 | [![GitHub release](https://img.shields.io/github/release/jeffctown/xcframework.svg?style=for-the-badge)](https://github.com/jeffctown/xcframework/releases) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg?style=for-the-badge)](https://raw.githubusercontent.com/jeffctown/xcframework/master/LICENSE.md) 6 | 7 | xcframework is a tool to create XCFrameworks. 8 | 9 | ## Installation 10 | 11 | ### Using a pre-built package: 12 | 13 | You can install xcframework by downloading `xcframework.pkg` from the 14 | [latest GitHub release](https://github.com/jeffctown/xcframework/releases/latest) and 15 | running it. 16 | 17 | ### Installing from source: 18 | 19 | You can also install from source by cloning this project and running 20 | `make install` (Xcode 11.0 beta 1 or later). Note: Running `make install` requires sudo permission to install the final executable. 21 | 22 | ### Compiling from source: 23 | 24 | You can build from source and use the executable without installation if you prefer to. Run `make installables` to output the final executable to `./.build/release/xcframework`. Feel free to use or copy the executable how you like. 25 | 26 | ## Quick Start 27 | 28 | * Create an XCFramework including a framework with iOS, tvOS, and watchOS: 29 | 30 | ```bash 31 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS --tvos PMLog_TvOS --watchos PMLog_WatchOS 32 | ``` 33 | 34 | ## Usage 35 | 36 | 37 | ### Help 38 | 39 | ``` 40 | $ xcframework help 41 | Available commands: 42 | 43 | build Build an XCFramework 44 | help Display general or command-specific help 45 | version Display the current version of xcframework 46 | ``` 47 | 48 | ### Build 49 | 50 | 51 | #### Build with Verbose Logging Enabled 52 | 53 | ```bash 54 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS --tvos PMLog_TvOS --watchos PMLog_WatchOS --verbose 55 | ``` 56 | 57 | #### Build with Output Directory Specified 58 | 59 | ```bash 60 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS --tvos PMLog_TvOS --watchos PMLog_WatchOS --output ./output 61 | ``` 62 | 63 | #### Build with Build Directory Specified 64 | 65 | ```bash 66 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS --tvos PMLog_TvOS --watchos PMLog_WatchOS --build ./build 67 | ``` 68 | 69 | #### Build with Extra xcodebuild Arguments 70 | 71 | Any arguments at the end of your command will be passed along to `xcodebuild` during archive. 72 | 73 | ```bash 74 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS DEBUG=1 PERFORM_MAGIC=0 75 | ``` 76 | 77 | 78 | ## Known Issues 79 | 80 | If you need to pass an xcodebuild argument that begins with a `-`, like `-configuration Release`, you will need to put a `--` before it. `--` tells this program (or tells [Commandant](https://github.com/Carthage/Commandant/issues/59)) to stop looking for named arguments. 81 | 82 | Without `--`: 83 | 84 | ```bash 85 | $ xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS -configuration Release 86 | Unrecognized arguments: -configurat 87 | ``` 88 | 89 | With `--`: 90 | 91 | ```bash 92 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS -- -configuration Release 93 | ``` 94 | 95 | ```bash 96 | xcframework build --project PMLog/PMLog.xcodeproj --name PMLog --ios PMLog_iOS -- -enableAddressSanitizer YES 97 | ``` 98 | 99 | 100 | ## License 101 | 102 | xcframework is released under the [MIT license](LICENSE.md). 103 | -------------------------------------------------------------------------------- /Sources/XCFrameworkKit/Version.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Version.swift 3 | // xcframework 4 | // 5 | // Created by Jeff Lett on 6/7/19. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Version { 11 | public let value: String 12 | public static let current = Version(value: "0.2.3") 13 | } 14 | -------------------------------------------------------------------------------- /Sources/XCFrameworkKit/XCFrameworkAssembler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCFrameworkAssembler.swift 3 | // 4 | // 5 | // Created by Antonino Urbano on 2020-01-11. 6 | // 7 | 8 | import Foundation 9 | import Shell 10 | 11 | public struct XCFrameworkAssembler { 12 | 13 | public var name: String? 14 | public var outputDirectory: String? 15 | public var frameworkPaths: [String]? 16 | 17 | private struct Framework { 18 | 19 | let path: String 20 | let name: String 21 | let archs: [String] 22 | let temporary: Bool 23 | 24 | var binaryPath: String { 25 | path+"/"+name 26 | } 27 | } 28 | 29 | private func frameworks(from paths: [String]) -> Result<[Framework], XCFrameworkAssemblerError> { 30 | 31 | var frameworks = [Framework]() 32 | for path in paths { 33 | guard path.hasSuffix(".framework") else { 34 | print(paths) 35 | return .failure(.invalidFrameworks) 36 | } 37 | if let binaryName = path.split(separator: "/").last?.split(separator: ".").first { 38 | let binaryPath = path + "/" + binaryName 39 | let archsResult = shell.usr.bin.xcrun.dynamicallyCall(withArguments: ["lipo", binaryPath, "-archs"]) 40 | if !archsResult.isSuccess { 41 | return .failure(.other("Couldn't parse the framework paths: \(archsResult.stderr)")) 42 | } 43 | if let archs = archsResult.stdout 44 | .split(separator: "\n") 45 | .first? 46 | .split(separator: " ") 47 | .compactMap({ String.init($0) }) { 48 | frameworks.append(XCFrameworkAssembler.Framework(path: path, name: String(binaryName), archs: archs, temporary: false)) 49 | } 50 | } 51 | } 52 | return .success(frameworks) 53 | } 54 | 55 | public enum XCFrameworkAssemblerError: Error { 56 | case nameNotFound 57 | case frameworksNotFound 58 | case invalidFrameworks 59 | case outputDirectoryNotFound 60 | case other(String) 61 | 62 | public var description: String { 63 | switch self { 64 | case .nameNotFound: 65 | return "No name parameter found." 66 | case .frameworksNotFound: 67 | return "No frameworks specified." 68 | case .invalidFrameworks: 69 | return "One or more of the passed in frameworks is not a valid .framework file, or the specified path was wrong." 70 | case .outputDirectoryNotFound: 71 | return "No output directory found." 72 | case .other(let stderr): 73 | return stderr 74 | } 75 | } 76 | } 77 | 78 | public init(name: String?, outputDirectory: String?, frameworkPaths: [String]?) { 79 | 80 | self.name = name 81 | self.outputDirectory = outputDirectory 82 | self.frameworkPaths = frameworkPaths 83 | } 84 | 85 | public mutating func assemble() -> Result<(),XCFrameworkAssemblerError> { 86 | 87 | guard let name = name else { 88 | return .failure(.nameNotFound) 89 | } 90 | 91 | guard let outputDirectory = outputDirectory else { 92 | return .failure(.outputDirectoryNotFound) 93 | } 94 | 95 | guard let frameworkPaths = frameworkPaths, frameworkPaths.count > 0 else { 96 | return .failure(.frameworksNotFound) 97 | } 98 | 99 | var frameworks = [Framework]() 100 | switch self.frameworks(from: frameworkPaths) { 101 | case .success(let result): 102 | frameworks = result 103 | case .failure(let error): 104 | return .failure(error) 105 | } 106 | 107 | guard frameworks.count > 0 else { 108 | print("count is 0") 109 | return .failure(.invalidFrameworks) 110 | } 111 | 112 | let finalOutputDirectory = outputDirectory.hasSuffix("/") ? outputDirectory : outputDirectory + "/" 113 | let finalOutput = finalOutputDirectory + name + ".xcframework" 114 | shell.bin.rm("-r",finalOutput) 115 | 116 | //duplicate the frameworks per-architecture and then create an xcframework from them 117 | var thinnedFrameworks = [Framework]() 118 | for framework in frameworks { 119 | print("Thinning framework \(framework.name) with archs: \(framework.archs)") 120 | if framework.archs.count <= 1 { 121 | thinnedFrameworks.append(framework) 122 | } else { 123 | for arch in framework.archs { 124 | let thinnedFrameworkPath = "\(framework.path)/../\(framework.name)_\(arch).framework" 125 | let thinnedFramework = Framework(path: thinnedFrameworkPath, name: framework.name, archs: [arch], temporary: true) 126 | shell.bin.rm("-r",thinnedFramework.path) 127 | let copyResult = shell.bin.cp("-R", framework.path, thinnedFramework.path) 128 | if !copyResult.isSuccess { 129 | return Result.failure(.other(copyResult.stderr)) 130 | } 131 | let thinnedResult = shell.usr.bin.xcrun.dynamicallyCall(withArguments: ["lipo", thinnedFramework.binaryPath, "-thin", arch, "-output", thinnedFramework.binaryPath]) 132 | if !thinnedResult.isSuccess { 133 | return Result.failure(.other(thinnedResult.stderr)) 134 | } 135 | thinnedFrameworks.append(thinnedFramework) 136 | } 137 | } 138 | } 139 | 140 | print("All thinned variants created, creating xcframework...") 141 | 142 | var arguments = [ 143 | "-create-xcframework", 144 | "-output", 145 | finalOutput, 146 | ] 147 | 148 | for thinnedFramework in thinnedFrameworks { 149 | arguments.append("-framework") 150 | arguments.append(thinnedFramework.path) 151 | } 152 | 153 | let xcframeworkResult = shell.usr.bin.xcodebuild.dynamicallyCall(withArguments: arguments) 154 | 155 | print("Cleaning up...") 156 | for thinnedFramework in thinnedFrameworks { 157 | if thinnedFramework.temporary { 158 | shell.bin.rm("-r",thinnedFramework.path) 159 | } 160 | } 161 | 162 | if !xcframeworkResult.isSuccess || !xcframeworkResult.stderr.isEmpty { 163 | return Result.failure(.other("xcframework creation failed. \nArguments: \(arguments.joined(separator: " "))\nError: \(xcframeworkResult.stderr)")) 164 | } 165 | 166 | print("Successfully created \(name).xcframework") 167 | 168 | return .success(()) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Sources/XCFrameworkKit/XCFrameworkBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCFrameworkBuilder.swift 3 | // XCFrameworkKit 4 | // 5 | // Created by Jeff Lett on 6/8/19. 6 | // 7 | 8 | import Foundation 9 | import Shell 10 | 11 | public class XCFrameworkBuilder { 12 | public var name: String? 13 | public var project: String? 14 | public var outputDirectory: String? 15 | public var buildDirectory: String? 16 | public var iOSScheme: String? 17 | public var watchOSScheme: String? 18 | public var tvOSScheme: String? 19 | public var macOSScheme: String? 20 | public var verbose: Bool = false 21 | public var compilerArguments: [String]? 22 | 23 | public enum XCFrameworkError: Error { 24 | case nameNotFound 25 | case projectNotFound 26 | case noSchemesFound 27 | case buildDirectoryNotFound 28 | case outputDirectoryNotFound 29 | case buildError(String) 30 | 31 | public var description: String { 32 | switch self { 33 | case .nameNotFound: 34 | return "No name parameter found." 35 | case .projectNotFound: 36 | return "No project parameter found." 37 | case .noSchemesFound: 38 | return "No schemes found." 39 | case .buildDirectoryNotFound: 40 | return "No build directory found." 41 | case .outputDirectoryNotFound: 42 | return "No output directory found." 43 | case .buildError(let stderr): 44 | return stderr 45 | } 46 | } 47 | } 48 | 49 | private enum SDK: String { 50 | case iOS = "iphoneos" 51 | case watchOS = "watchos" 52 | case tvOS = "appletvos" 53 | case macOS = "macosx" 54 | case iOSSim = "iphonesimulator" 55 | case watchOSSim = "watchsimulator" 56 | case tvOSSim = "appletvsimulator" 57 | } 58 | 59 | public init(configure: (XCFrameworkBuilder) -> ()) { 60 | configure(self) 61 | } 62 | 63 | public func build() -> Result<(),XCFrameworkError> { 64 | 65 | guard let name = name else { 66 | return .failure(XCFrameworkError.nameNotFound) 67 | } 68 | 69 | guard let project = project else { 70 | return .failure(XCFrameworkError.projectNotFound) 71 | } 72 | 73 | guard watchOSScheme != nil || iOSScheme != nil || macOSScheme != nil || tvOSScheme != nil else { 74 | return .failure(XCFrameworkError.noSchemesFound) 75 | } 76 | 77 | guard let outputDirectory = outputDirectory else { 78 | return .failure(XCFrameworkError.outputDirectoryNotFound) 79 | } 80 | 81 | guard let buildDirectory = buildDirectory else { 82 | return .failure(XCFrameworkError.buildDirectoryNotFound) 83 | } 84 | 85 | print("Creating \(name)...") 86 | 87 | //final build location 88 | let finalBuildDirectory = buildDirectory.hasSuffix("/") ? buildDirectory : buildDirectory + "/" 89 | 90 | //final xcframework location 91 | let finalOutputDirectory = outputDirectory.hasSuffix("/") ? outputDirectory : outputDirectory + "/" 92 | let finalOutput = finalOutputDirectory + name + ".xcframework" 93 | 94 | shell.usr.rm(finalOutput) 95 | //array of arguments for the final xcframework construction 96 | var frameworksArguments = ["-create-xcframework"] 97 | 98 | //try all supported SDKs 99 | do { 100 | if let watchOSScheme = watchOSScheme { 101 | try frameworksArguments.append(contentsOf: buildScheme(scheme: watchOSScheme, sdk: .watchOS, project: project, name: name, buildPath: finalBuildDirectory)) 102 | try frameworksArguments.append(contentsOf: buildScheme(scheme: watchOSScheme, sdk: .watchOSSim, project: project, name: name, buildPath: finalBuildDirectory)) 103 | } 104 | 105 | if let iOSScheme = iOSScheme { 106 | try frameworksArguments.append(contentsOf: buildScheme(scheme: iOSScheme, sdk: .iOS, project: project, name: name, buildPath: finalBuildDirectory)) 107 | try frameworksArguments.append(contentsOf: buildScheme(scheme: iOSScheme, sdk: .iOSSim, project: project, name: name, buildPath: finalBuildDirectory)) 108 | } 109 | 110 | if let tvOSScheme = tvOSScheme { 111 | try frameworksArguments.append(contentsOf: buildScheme(scheme: tvOSScheme, sdk: .tvOS, project: project, name: name, buildPath: finalBuildDirectory)) 112 | try frameworksArguments.append(contentsOf: buildScheme(scheme: tvOSScheme, sdk: .tvOSSim, project: project, name: name, buildPath: finalBuildDirectory)) 113 | } 114 | 115 | if let macOSScheme = macOSScheme { 116 | try frameworksArguments.append(contentsOf: buildScheme(scheme: macOSScheme, sdk: .macOS, project: project, name: name, buildPath: finalBuildDirectory)) 117 | } 118 | } catch let error as XCFrameworkError { 119 | return .failure(error) 120 | } catch { 121 | return .failure(.buildError(error.localizedDescription)) 122 | } 123 | 124 | print("Combining...") 125 | //add output to final command 126 | frameworksArguments.append("-output") 127 | frameworksArguments.append(finalOutput) 128 | if verbose { 129 | print("xcodebuild \(frameworksArguments.joined(separator: " "))") 130 | } 131 | let result = shell.usr.bin.xcodebuild.dynamicallyCall(withArguments: frameworksArguments) 132 | if !result.isSuccess { 133 | return .failure(.buildError(result.stderr + "\nXCFramework Build Error From Running: 'xcodebuild \(frameworksArguments.joined(separator: " "))'")) 134 | } 135 | print("Success. \(finalOutput)") 136 | return .success(()) 137 | } 138 | 139 | private func buildScheme(scheme: String, sdk: SDK, project: String, name: String, buildPath: String) throws -> [String] { 140 | print("Building scheme \(scheme) for \(sdk.rawValue)...") 141 | var frameworkArguments = [String]() 142 | //path for each scheme's archive 143 | let archivePath = buildPath + "\(scheme)-\(sdk.rawValue).xcarchive" 144 | //array of arguments for the archive of each framework 145 | //weird interpolation errors are forcing me to use this "" + syntax. not sure if this is a compiler bug or not. 146 | var archiveArguments = ["-project", "\"" + project + "\"", "-scheme", "\"" + scheme + "\"", "archive", "SKIP_INSTALL=NO", "BUILD_LIBRARY_FOR_DISTRIBUTION=YES"] 147 | if let compilerArguments = compilerArguments { 148 | archiveArguments.append(contentsOf: compilerArguments) 149 | } 150 | archiveArguments.append(contentsOf: ["-archivePath", archivePath, "-sdk", sdk.rawValue]) 151 | if verbose { 152 | print(" xcodebuild \(archiveArguments.joined(separator: " "))") 153 | } 154 | let result = shell.usr.bin.xcodebuild.dynamicallyCall(withArguments: archiveArguments) 155 | if !result.isSuccess { 156 | let errorMessage = result.stderr + "\nArchive Error From Running: 'xcodebuild \(archiveArguments.joined(separator: " "))'" 157 | throw XCFrameworkError.buildError(errorMessage) 158 | } 159 | //add this framework to the list for the final output command 160 | frameworkArguments.append("-framework") 161 | frameworkArguments.append(archivePath + "/Products/Library/Frameworks/\(name).framework") 162 | return frameworkArguments 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /Sources/xcframework/Commands/AssembleCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssembleCommand.swift 3 | // 4 | // 5 | // Created by Antonino Urbano on 2020-01-11. 6 | // 7 | 8 | import Commandant 9 | import Foundation 10 | import Shell 11 | import XCFrameworkKit 12 | 13 | struct AssembleCommand: CommandProtocol { 14 | 15 | static let frameworkSuffix = ".framework" 16 | 17 | // MARK: - CommandProtocol 18 | 19 | var verb = "assemble" 20 | var function = "Assembles an xcframework from pre-built frameworks. If the framework(s) passed in are fat binaries containing multiple architecturees, they will first be split apart." 21 | 22 | // MARK: - OptionsProtocol 23 | 24 | struct Options: OptionsProtocol { 25 | let name: String? 26 | let outputDirectory: String 27 | let frameworks: String? 28 | 29 | static func create(_ name: String?) -> (String) -> (String?) -> Options { 30 | return { outputDirectory in { frameworks in Options(name: name, outputDirectory: outputDirectory, frameworks: frameworks ) } } 31 | } 32 | 33 | static func evaluate(_ mode: CommandMode) -> Result>> { 34 | return create 35 | <*> mode <| Option(key: "name", defaultValue: nil, usage: "REQUIRED: the framework name, Example: .framework") 36 | <*> mode <| Option(key: "output", defaultValue: FileManager.default.currentDirectoryPath, usage: "the output directory (default: .)") 37 | <*> mode <| Option(key: "frameworks", defaultValue: nil, usage: "the pre-build frameworks to assemble into an xcframework") 38 | } 39 | } 40 | 41 | func run(_ options: Options) -> Result<(), CommandantError<()>> { 42 | 43 | var sanitizedFrameworks = options.frameworks?.components(separatedBy: "\(Self.frameworkSuffix) ") 44 | if sanitizedFrameworks?.count ?? 0 > 1 { 45 | sanitizedFrameworks = sanitizedFrameworks?.map { !$0.hasSuffix(Self.frameworkSuffix) ? $0+Self.frameworkSuffix : $0 } 46 | } 47 | 48 | var builder = XCFrameworkAssembler.init(name: options.name, outputDirectory: options.outputDirectory, frameworkPaths: sanitizedFrameworks) 49 | 50 | let result = builder.assemble() 51 | switch result { 52 | case .success(): 53 | return .success(()) 54 | case .failure(let error): 55 | switch error { 56 | case .other: 57 | return .failure(.usageError(description: error.description)) 58 | default: 59 | return .failure(.usageError(description: error.description + "\n Please run 'xcframework help assemble' to see the full list of parameters for this command.")) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/xcframework/Commands/BuildCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BuildCommand.swift 3 | // xcframework 4 | // 5 | // Created by Jeff Lett on 6/7/19. 6 | // 7 | 8 | import Commandant 9 | import Foundation 10 | import Shell 11 | import XCFrameworkKit 12 | 13 | struct BuildCommand: CommandProtocol { 14 | 15 | // MARK: - CommandProtocol 16 | 17 | let verb = "build" 18 | let function = "Build an XCFramework" 19 | 20 | // MARK: - OptionsProtocol 21 | 22 | struct Options: OptionsProtocol { 23 | let project: String? 24 | let name: String? 25 | let outputDirectory: String 26 | let buildDirectory: String 27 | let iOSScheme: String? 28 | let watchOSScheme: String? 29 | let tvOSScheme: String? 30 | let macOSScheme: String? 31 | let verbose: Bool 32 | let compilerArguments: [String] 33 | 34 | static func create(_ project: String?) -> (String?) -> (String) -> (String) -> (String?) -> (String?) -> (String?) -> (String?) -> (Bool) -> ([String]) -> Options { 35 | return { name in { outputDirectory in { buildDirectory in { iOSScheme in { watchOSScheme in { tvOSScheme in { macOSScheme in { verbose in { compilerArguments in Options(project: project, name: name, outputDirectory: outputDirectory, buildDirectory: buildDirectory, iOSScheme: iOSScheme, watchOSScheme: watchOSScheme, tvOSScheme: tvOSScheme, macOSScheme: macOSScheme, verbose: verbose, compilerArguments: compilerArguments) } } } } } } } } } 36 | } 37 | 38 | static func evaluate(_ mode: CommandMode) -> Result>> { 39 | let defaultBuildDirectory = "/tmp/xcframework/build/" 40 | return create 41 | <*> mode <| Option(key: "project", defaultValue: nil, usage: "REQUIRED: the path and project to build") 42 | <*> mode <| Option(key: "name", defaultValue: nil, usage: "REQUIRED: the framework name, Example: .framework") 43 | <*> mode <| Option(key: "output", defaultValue: FileManager.default.currentDirectoryPath, usage: "the output directory (default: .)") 44 | <*> mode <| Option(key: "build", defaultValue: FileManager.default.currentDirectoryPath.appending(defaultBuildDirectory), usage: "build directory (default: \(defaultBuildDirectory)") 45 | <*> mode <| Option(key: "ios", defaultValue: nil, usage: "the scheme for your iOS target") 46 | <*> mode <| Option(key: "watchos", defaultValue: nil, usage: "the scheme for your watchOS target") 47 | <*> mode <| Option(key: "tvos", defaultValue: nil, usage: "the scheme for your tvOS target") 48 | <*> mode <| Option(key: "macos", defaultValue: nil, usage: "the scheme for your macOS target") 49 | <*> mode <| Switch(key: "verbose", usage: "enable verbose logs") 50 | <*> mode <| Argument(defaultValue: [], usage: "any extra xcodebuild arguments to be used in the framework archiving") 51 | } 52 | } 53 | 54 | func run(_ options: Options) -> Result<(), CommandantError<()>> { 55 | let builder = XCFrameworkBuilder() { builder in 56 | builder.name = options.name 57 | builder.project = options.project 58 | builder.outputDirectory = options.outputDirectory 59 | builder.buildDirectory = options.buildDirectory 60 | builder.iOSScheme = options.iOSScheme 61 | builder.watchOSScheme = options.watchOSScheme 62 | builder.tvOSScheme = options.tvOSScheme 63 | builder.macOSScheme = options.macOSScheme 64 | builder.verbose = options.verbose 65 | builder.compilerArguments = options.compilerArguments 66 | } 67 | let result = builder.build() 68 | switch result { 69 | case .success(): 70 | return .success(()) 71 | case .failure(let error): 72 | switch error { 73 | case .buildError: 74 | return .failure(.usageError(description: error.description)) 75 | default: 76 | return .failure(.usageError(description: error.description + "\n Please run 'xcframework help build' to see the full list of parameters for this command.")) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/xcframework/Commands/VersionCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VersionCommand.swift 3 | // xcframework 4 | // 5 | // Created by Jeff Lett on 6/7/19. 6 | // 7 | 8 | import Commandant 9 | import Foundation 10 | import XCFrameworkKit 11 | 12 | struct VersionCommand: CommandProtocol { 13 | // MARK: - CommandProtocol 14 | 15 | let verb = "version" 16 | let function = "Display the current version of xcframework" 17 | 18 | func run(_ options: NoOptions>) -> Result<(), CommandantError<()>> { 19 | print(Version.current.value) 20 | return .success(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/xcframework/main.swift: -------------------------------------------------------------------------------- 1 | import Commandant 2 | 3 | let registry = CommandRegistry>() 4 | registry.register(BuildCommand()) 5 | registry.register(AssembleCommand()) 6 | registry.register(VersionCommand()) 7 | 8 | let helpCommand = HelpCommand(registry: registry) 9 | registry.register(helpCommand) 10 | registry.main(defaultVerb: helpCommand.verb) { error in 11 | print(error) 12 | } 13 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import xcframeworkTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += xcframeworkTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/XCFrameworkKitTests/test.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffctown/xcframework/dc509d42747990860a0535511f404970d29e79d2/Tests/XCFrameworkKitTests/test.swift -------------------------------------------------------------------------------- /Tests/xcframeworkTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(xcframeworkTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/xcframeworkTests/xcframeworkTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class xcframeworkTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("xcframework") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample), 46 | ] 47 | } 48 | --------------------------------------------------------------------------------