#!/usr/bin/env xcrun swift /* * Copyright 2017 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Foundation enum Colors: String { case black = "\u{001B}[0;30m" case red = "\u{001B}[0;31m" case green = "\u{001B}[0;32m" case yellow = "\u{001B}[0;33m" case blue = "\u{001B}[0;34m" case magenta = "\u{001B}[0;35m" case cyan = "\u{001B}[0;36m" case white = "\u{001B}[0;37m" } func colorPrint(color: Colors, text: String) { print(color.rawValue + text + "\u{001B}[0;0m") } enum Platform: String { case iOS case macOS case tvOS case watchOS } let allFrameworks: [String: [Platform]] = [ "FirebaseAuth": [.iOS, .macOS], "FirebaseCore": [.iOS, .macOS], "FirebaseDatabase": [.iOS, .macOS], "FirebaseMessaging": [.iOS], "FirebaseStorage": [.iOS, .macOS] ] let currentDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) let url = URL(fileURLWithPath: CommandLine.arguments[0], relativeTo: currentDirectoryURL) let commandPath = url.deletingLastPathComponent().path func usage() -> Never { print("usage: ./build.swift -f {framework1} -f {framework2} ....") print("usage: ./build.swift -all") print("Valid frameworks are \(allFrameworks)") exit(1) } func processOptions() -> [String] { guard CommandLine.arguments.count > 1 else { usage() } var doFrameworks = [String]() var optIndex = 1 whileLoop: while optIndex < CommandLine.arguments.count { switch CommandLine.arguments[optIndex] { case "-all": guard doFrameworks.count == 0, CommandLine.arguments.count == 2 else { colorPrint(color:Colors.red, text:"-all must be a solo option") usage() } doFrameworks = Array(allFrameworks.keys) break whileLoop case "-f": optIndex += 1 guard optIndex < CommandLine.arguments.count else { colorPrint(color:Colors.red, text:"The -f option must be followed by a framework name") usage() } let framework = CommandLine.arguments[optIndex] guard allFrameworks.keys.contains(framework) else { colorPrint(color:Colors.red, text:"\(framework) is not a valid framework") usage() } doFrameworks += [framework] optIndex += 1 default: colorPrint(color:Colors.red, text: "Invalid option: \(CommandLine.arguments[optIndex])") usage() } } return doFrameworks } func tempDir() -> String { let directory = NSTemporaryDirectory() let fileName = NSUUID().uuidString guard let dir = NSURL.fileURL(withPathComponents:[directory, fileName]) else { colorPrint(color:Colors.red, text:"Failed to create temp directory") exit(1) } return dir.path } func syncExec(command: String, args: [String] = []) { let task = Process() task.launchPath = command task.arguments = args task.currentDirectoryPath = commandPath task.launch() task.waitUntilExit() guard (task.terminationStatus == 0) else { colorPrint(color:Colors.red, text:"Command failed:") colorPrint(color:Colors.red, text:command + " " + args.joined(separator:" ")) exit(1) } } func buildThin(framework: String, multiplatform: Bool, arch: String, multisdk: Bool, sdk: String, parentDir: String) -> [String] { let schemeSuffix: String if !multiplatform { schemeSuffix = "" } else if sdk.hasPrefix("mac") { schemeSuffix = "-OSX" } else if sdk.hasPrefix("iphone") { schemeSuffix = "-iOS" } else { fatalError("TODO: tvOS/watchOS") } let buildDir = parentDir + "/" + arch let standardOptions = [ "build", "-configuration", "release", "-workspace", "FrameworkMaker.xcworkspace", "-scheme", framework + schemeSuffix, "GCC_GENERATE_DEBUGGING_SYMBOLS=No"] let bitcode = (sdk == "iphoneos") ? ["OTHER_CFLAGS=\"" + "-fembed-bitcode\""] : [] let args = standardOptions + ["ARCHS=" + arch, "BUILD_DIR=" + buildDir, "-sdk", sdk] + bitcode syncExec(command:"/usr/bin/xcodebuild", args:args) return [buildDir + "/Release" + (multisdk ? "-\(sdk)" : "") + "/" + framework + schemeSuffix + "/lib" + framework + schemeSuffix + ".a"] } func createFile(file: String, content: String) { let data = content.data(using:String.Encoding.utf8) guard FileManager.default.createFile(atPath:file, contents: data, attributes: nil) else { print("Error creating " + file) exit(1) } } // TODO: Add support for adding library and framework dependencies to makeModuleMap func makeModuleMap(framework: String, dir: String) { let moduleDir = dir + "/Modules" syncExec(command:"/bin/mkdir", args:["-p", moduleDir]) let moduleFile = moduleDir + "/module.modulemap" let content = "framework module " + framework + " {\n" + " umbrella header \"" + framework + ".h\"\n" + " export *\n" + " module * { export *}\n" + "}\n" createFile(file:moduleFile, content:content) } func buildFramework(withName framework: String, multiplatform: Bool, platform: Platform, outputDir: String) { let buildDir = tempDir() var thinArchives = [String]() switch platform { case .iOS: thinArchives += buildThin(framework:framework, multiplatform: multiplatform, arch:"arm64", multisdk: true, sdk:"iphoneos", parentDir:buildDir) thinArchives += buildThin(framework:framework, multiplatform: multiplatform, arch:"armv7", multisdk: true, sdk:"iphoneos", parentDir:buildDir) thinArchives += buildThin(framework:framework, multiplatform: multiplatform, arch:"i386", multisdk: true, sdk:"iphonesimulator", parentDir:buildDir) thinArchives += buildThin(framework:framework, multiplatform: multiplatform, arch:"x86_64", multisdk: true, sdk:"iphonesimulator", parentDir:buildDir) case .macOS: thinArchives += buildThin(framework:framework, multiplatform: multiplatform, arch:"x86_64", multisdk: false, sdk:"macosx", parentDir:buildDir) default: fatalError("TODO: tvOS/watchOS") } let frameworkDir = outputDir + "/" + framework + "_" + platform.rawValue + ".framework" syncExec(command:"/bin/mkdir", args:["-p", frameworkDir]) let fatArchive = frameworkDir + "/" + framework syncExec(command:"/usr/bin/lipo", args:["-create", "-output", fatArchive] + thinArchives) syncExec(command:"/bin/rm", args:["-rf"] + thinArchives) let headersDir = frameworkDir + "/Headers" syncExec(command:"/bin/mv", args:[NSString(string:thinArchives[0]).deletingLastPathComponent, headersDir]) syncExec(command:"/bin/rm", args:["-rf", buildDir]) makeModuleMap(framework:framework, dir:frameworkDir) } let frameworks = processOptions() colorPrint(color:Colors.green, text:"Building \(frameworks)") let outputDir = tempDir() syncExec(command:"/usr/local/bin/pod", args:["update"]) for f in frameworks { let platforms = allFrameworks[f]! for p in platforms { buildFramework(withName:f, multiplatform:platforms.count > 1, platform:p, outputDir:outputDir) } } print() colorPrint(color:Colors.magenta, text:"The frameworks are available at the locations below:") syncExec(command:"/usr/bin/find", args:[outputDir, "-depth", "1"])