From f4ab0611a62d04f33cd14b64591285a88fb53056 Mon Sep 17 00:00:00 2001 From: Darryl Pogue Date: Thu, 26 Oct 2017 10:27:56 -0700 Subject: [PATCH] Updated for Xcode 8 Most of these changes came from https://github.com/stepanhruda/ios-simulator-app-installer/pull/30 with further additions. Unfortunately those were all done in a working copy without applying to git, so this commit doesn't reflect proper attribution to the author(s). --- Formula/ios-simulator-app-installer.rb | 9 +- app-package-launcher/AppDelegate.swift | 10 +- app-package-launcher/Installer.swift | 18 +- app-package-launcher/PackagedApp.swift | 32 +- app-package-launcher/Shell.swift | 295 +++++++++++++++++- app-package-launcher/Simulator.swift | 20 +- .../SimulatorSelectionViewController.swift | 12 +- .../project.pbxproj | 12 +- .../xcschemes/app-package-launcher.xcscheme | 2 +- src/Arguments.swift | 30 +- src/Packaging.swift | 81 +++-- src/Shell.swift | 295 +++++++++++++++++- src/Simulator.swift | 18 +- src/StringToCChar.swift | 4 +- src/Xcode.swift | 4 +- .../project.pbxproj | 12 +- .../ios-simulator-app-installer.xcscheme | 8 +- src/main.swift | 4 +- 18 files changed, 734 insertions(+), 132 deletions(-) diff --git a/Formula/ios-simulator-app-installer.rb b/Formula/ios-simulator-app-installer.rb index d4632f6..075b258 100644 --- a/Formula/ios-simulator-app-installer.rb +++ b/Formula/ios-simulator-app-installer.rb @@ -1,10 +1,9 @@ class IosSimulatorAppInstaller < Formula - homepage "https://github.com/stepanhruda/ios-simulator-app-installer" - url "https://github.com/stepanhruda/ios-simulator-app-installer.git", - :revision => "def460846f6065e8d0bbdf8ad9bd5cd01ca25e0c" - version "0.2.1" + homepage "https://github.com/tcurdt/ios-simulator-app-installer" + url "https://github.com/tcurdt/ios-simulator-app-installer.git", :tag => "0.3.0" + version "0.3.0" - depends_on :xcode => "7" + depends_on :xcode => "8" depends_on :macos => :yosemite def install diff --git a/app-package-launcher/AppDelegate.swift b/app-package-launcher/AppDelegate.swift index e70dad0..cf23f2c 100644 --- a/app-package-launcher/AppDelegate.swift +++ b/app-package-launcher/AppDelegate.swift @@ -4,7 +4,7 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { - func applicationDidFinishLaunching(notification: NSNotification) { + func applicationDidFinishLaunching(_ notification: Notification) { do { let packagedApp = try PackagedApp(bundleName: "Packaged") @@ -37,7 +37,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var simulatorSelectionController: SimulatorSelectionWindowController? - func letUserSelectSimulatorFrom(simulators: [Simulator], completion: Simulator -> Void) { + func letUserSelectSimulatorFrom(_ simulators: [Simulator], completion: @escaping (Simulator) -> Void) { simulatorSelectionController = SimulatorSelectionWindowController.controller(simulators) { [unowned self] selectedSimulator in completion(selectedSimulator) @@ -46,12 +46,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { simulatorSelectionController?.showWindow(nil) } - func terminateWithError(error: NSError) { + func terminateWithError(_ error: NSError) { NSAlert(error: error).runModal() - NSApplication.sharedApplication().terminate(nil) + NSApplication.shared().terminate(nil) } - func noSuitableDeviceFoundForStringError(targetDevice: String) -> NSError { + func noSuitableDeviceFoundForStringError(_ targetDevice: String) -> NSError { return NSError( domain: "com.stepanhruda.ios-simulator-app-installer", code: 2, diff --git a/app-package-launcher/Installer.swift b/app-package-launcher/Installer.swift index ec05b10..dc0a3c6 100644 --- a/app-package-launcher/Installer.swift +++ b/app-package-launcher/Installer.swift @@ -3,26 +3,26 @@ import Foundation class Installer { - static func installAndRunApp(packagedApp: PackagedApp, simulator: Simulator) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { + static func installAndRunApp(_ packagedApp: PackagedApp, simulator: Simulator) { + DispatchQueue.global().async { shutDownCurrentSimulatorSessions() - system("xcrun instruments -w \"\(simulator.identifierString)\"") + _ = Shell.run(command: "xcrun instruments -w \"\(simulator.identifierString)\"") if Parameters.shouldUninstallFirst() { - system("xcrun simctl uninstall booted \(packagedApp.bundleIdentifier)") + _ = Shell.run(command: "xcrun simctl uninstall booted \(packagedApp.bundleIdentifier)") } - system("xcrun simctl install booted \"\(packagedApp.bundlePath)\"") - system("xcrun simctl launch booted \(packagedApp.bundleIdentifier)") + _ = Shell.run(command: "xcrun simctl install booted \"\(packagedApp.bundlePath)\"") + _ = Shell.run(command: "xcrun simctl launch booted \(packagedApp.bundleIdentifier)") - NSApplication.sharedApplication().terminate(nil) + NSApplication.shared().terminate(nil) } } static func shutDownCurrentSimulatorSessions() { - system("killall \"iOS Simulator\"") - system("xcrun simctl shutdown booted") + _ = Shell.run(command: "killall \"Simulator\"") + _ = Shell.run(command: "xcrun simctl shutdown booted") } } diff --git a/app-package-launcher/PackagedApp.swift b/app-package-launcher/PackagedApp.swift index 31a4a99..7cf557e 100644 --- a/app-package-launcher/PackagedApp.swift +++ b/app-package-launcher/PackagedApp.swift @@ -1,16 +1,16 @@ import Foundation -enum PackagedAppError: ErrorType { - case BundleNotFound - case InfoPlistNotFound - case BundleIdentifierNotFound +enum PackagedAppError: Error { + case bundleNotFound + case infoPlistNotFound + case bundleIdentifierNotFound var asNSError: NSError { let description: String switch self { - case .BundleNotFound: description = "App bundle couldn't be found, this installer was packaged incorrectly." - case .InfoPlistNotFound: description = "Info.plist not found in packaged app, this installer was packaged incorrectly." - case .BundleIdentifierNotFound: description = "Bundle identifier not found in packaged app's Info.plist, this installer was packaged incorrectly." + case .bundleNotFound: description = "App bundle couldn't be found, this installer was packaged incorrectly." + case .infoPlistNotFound: description = "Info.plist not found in packaged app, this installer was packaged incorrectly." + case .bundleIdentifierNotFound: description = "Bundle identifier not found in packaged app's Info.plist, this installer was packaged incorrectly." } return NSError( @@ -26,24 +26,24 @@ struct PackagedApp { let bundleIdentifier: String init(bundleName: String) throws { - guard let bundlePath = PackagedApp.pathForFileNamed(bundleName) else { throw PackagedAppError.BundleNotFound } - guard let infoPlist = PackagedApp.infoPlistInBundleWithPath(bundlePath) else { throw PackagedAppError.InfoPlistNotFound } - guard let bundleIdentifier = PackagedApp.bundleIdentifierFromInfoPlist(infoPlist) else { throw PackagedAppError.BundleIdentifierNotFound } + guard let bundlePath = PackagedApp.pathForFileNamed(bundleName) else { throw PackagedAppError.bundleNotFound } + guard let infoPlist = PackagedApp.infoPlistInBundleWithPath(bundlePath) else { throw PackagedAppError.infoPlistNotFound } + guard let bundleIdentifier = PackagedApp.bundleIdentifierFromInfoPlist(infoPlist) else { throw PackagedAppError.bundleIdentifierNotFound } self.bundlePath = bundlePath self.bundleName = bundleName self.bundleIdentifier = bundleIdentifier } - static func pathForFileNamed(filename: String) -> String? { - return NSBundle.mainBundle().pathForResource(filename, ofType: "app") + static func pathForFileNamed(_ filename: String) -> String? { + return Bundle.main.path(forResource: filename, ofType: "app") } - static func infoPlistInBundleWithPath(bundlePath: String) -> NSDictionary? { - return NSDictionary(contentsOfFile: bundlePath.stringByAppendingString("/Info.plist")) + static func infoPlistInBundleWithPath(_ bundlePath: String) -> NSDictionary? { + return NSDictionary(contentsOfFile: bundlePath + "/Info.plist") } - static func bundleIdentifierFromInfoPlist(infoPlist: NSDictionary) -> String? { - return infoPlist.objectForKey(kCFBundleIdentifierKey) as? String + static func bundleIdentifierFromInfoPlist(_ infoPlist: NSDictionary) -> String? { + return infoPlist.object(forKey: kCFBundleIdentifierKey) as? String } } diff --git a/app-package-launcher/Shell.swift b/app-package-launcher/Shell.swift index 9ddc87f..09b19bd 100644 --- a/app-package-launcher/Shell.swift +++ b/app-package-launcher/Shell.swift @@ -1,23 +1,296 @@ +import Foundation + class Shell { static func run(command: String) -> [String] { - let task = NSTask() - task.launchPath = "/bin/sh" - task.arguments = ["-c", command] + let task = ShellTask(launchPath: "/bin/sh") + + task.arguments = [ + "-c", + command + ] - let pipe = NSPipe() - task.standardOutput = pipe - task.standardError = pipe + var buffer = Data() + + task.outputOptions = [ + .handle { availableData in + buffer.append(availableData) + } + ] - let file = pipe.fileHandleForReading + task.launch { result in + + switch result { + case .success: + break + case let .failure(code): + print("command [\(task)] failed [\(code)]: \(command)") + } - task.launch() + } task.waitUntilExit() - let outputData = file.readDataToEndOfFile() - let outputString = NSString(data: outputData, encoding: NSUTF8StringEncoding) as? String - return outputString?.componentsSeparatedByString("\n") ?? [] + let outputString = NSString(data: buffer as Data, encoding: String.Encoding.utf8.rawValue) as? String + return outputString?.components(separatedBy: "\n") ?? [] + } +} + + +// baseed on code from iam Nichols +final class ShellTask { + + enum IOOption { + case print(prefix: String?) + case handle(callback: (_ availableData: Data) -> Void) + } + + enum Result { + case success + case failure(Int32) + } + + let launchPath: String + var arguments: [String]? = nil + var environment: [String: String]? = nil + var currentDirectoryPath: String? = nil + var outputOptions = [IOOption]() + var errorOptions = [IOOption]() + + init(launchPath: String) { + self.launchPath = launchPath + self.taskQueue.name = "ShellTask.LaunchQueue" + } + + deinit { + reset() + } + + fileprivate let taskQueue = OperationQueue() + fileprivate var notificationTokens = [NSObjectProtocol]() + fileprivate var task = Process() + fileprivate var errorPipe = Pipe() + fileprivate var outputPipe = Pipe() + fileprivate var errorReachedEOF = false + fileprivate var outputReachedEOF = false + fileprivate var errorRecievedData = false + fileprivate var outputRecievedData = false + fileprivate var completion: ((_ result: ShellTask.Result) -> Void)? + + var terminationStatus: Int32? = nil + + var command: String { + get { + return launchPath + " " + (arguments?.joined(separator: " "))! + } + } + + fileprivate func reset() { + + for token in notificationTokens { + NotificationCenter.default.removeObserver(token) + } + notificationTokens.removeAll() + + task = Process() + outputPipe = Pipe() + errorPipe = Pipe() + outputReachedEOF = false + errorReachedEOF = false + outputRecievedData = false + errorRecievedData = false + terminationStatus = nil + completion = nil + } + + var isRunning: Bool { + return completion != nil + } + + func launch(_ completion: @escaping (_ result: ShellTask.Result) -> Void) { + + if isRunning { + fatalError("The instance of ShellTask has already launched.") + } + + // print("[ShellTask] Launching:", launchPath, arguments?.joined(separator: " ") ?? "") + + self.completion = completion + + task.launchPath = launchPath + task.arguments = arguments + if let environment = environment { + task.environment = environment + } + task.standardOutput = outputPipe + task.standardError = errorPipe + if let currentDirectoryPath = self.currentDirectoryPath { + task.currentDirectoryPath = currentDirectoryPath + } + + taskQueue.addOperation(BlockOperation { + + self.setupPipe(self.outputPipe) + self.setupPipe(self.errorPipe) + + self.setupTerminationForTask(self.task) + + self.task.launch() + + self.waitUntilExit() + + DispatchQueue.main.async { + self.complete() + } + }) + } + + func waitUntilExit() { + while !self.hasCompleted() { + RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date.distantFuture) + } + } + + fileprivate func complete() { + + if let completion = self.completion, let terminationStatus = self.terminationStatus { + + if terminationStatus == EXIT_SUCCESS { + completion(.success) + } else { + completion(.failure(terminationStatus)) + } + + // self.reset() + + } else { + fatalError("completion or termination status aren't present but the task completed.") + } + } + + fileprivate func hasCompleted() -> Bool { + return outputReachedEOF && errorReachedEOF && terminationStatus != nil + } +} + +// MARK: - File Handles +private extension ShellTask { + + func setupPipe(_ pipe: Pipe) { + + let center = NotificationCenter.default + let name = NSNotification.Name.NSFileHandleDataAvailable + let handle = pipe.fileHandleForReading + let callback = fileHandleDataAvailableBlock + + handle.waitForDataInBackgroundAndNotify() + + let token = center.addObserver(forName: name, object: handle, queue: taskQueue, using: callback) + notificationTokens.append(token) + } + + func fileHandleDataAvailableBlock(_ notification: Notification) { + if let fileHandle = notification.object as? FileHandle { + fileHandleDataAvailable(fileHandle) + } + } + + func fileHandleDataAvailable(_ fileHandle: FileHandle) { + + let availableData = fileHandle.availableData + + if fileHandle === outputPipe.fileHandleForReading { + processData(availableData, withOptions: outputOptions, initialChunk: !outputRecievedData) + } else if fileHandle === errorPipe.fileHandleForReading { + processData(availableData, withOptions: errorOptions, initialChunk: !errorRecievedData) + } + + if availableData.count == 0 { + fileHandleReachedEOF(fileHandle) + } + + if fileHandle === outputPipe.fileHandleForReading && !outputRecievedData { + outputRecievedData = true + } else if fileHandle === errorPipe.fileHandleForReading && !errorRecievedData { + errorRecievedData = true + } + + fileHandle.waitForDataInBackgroundAndNotify() + } + + func fileHandleReachedEOF(_ fileHandle: FileHandle) { + + if fileHandle === outputPipe.fileHandleForReading { + outputReachedEOF = true + } else if fileHandle === errorPipe.fileHandleForReading { + errorReachedEOF = true + } + } +} + +// MARK: - IOOption +private extension ShellTask { + + func processData(_ availableData: Data, withOptions options: [IOOption], initialChunk: Bool) { + + for option in options { + + switch option { + case let .print(prefix): + // print the NSData via `print` + if let chunk = String(data: availableData, encoding: String.Encoding.utf8) { + printChunk(chunk, prefix: prefix, initialChunk: initialChunk, EOF: availableData.count == 0) + } + + case let .handle(callback): + // pass the data back through the closure + callback(availableData) + } + } + } + + func printChunk(_ chunk: String, prefix: String?, initialChunk: Bool, EOF: Bool) { + + if let prefix = prefix , EOF == false { + + var output = chunk + + if initialChunk == true { + output = prefix + " " + output + } + + output = output.replacingOccurrences(of: "\n", with: "\n" + prefix + " ") + + print(output, separator: "", terminator: "") + + } else { + + print(chunk, separator: "", terminator: EOF ? "\n" : "") + } + } +} + +// MARK: - Termination +private extension ShellTask { + + func setupTerminationForTask(_ task: Process) { + + let center = NotificationCenter.default + let name = Process.didTerminateNotification + let callback = taskDidTerminateBlock + + let token = center.addObserver(forName: name, object: task, queue: taskQueue, using: callback) + notificationTokens.append(token) + } + + func taskDidTerminateBlock(_ notification: Notification) { + if let task = notification.object as? Process { + taskDidTerminate(task) + } + } + + func taskDidTerminate(_ task: Process) { + terminationStatus = task.terminationStatus } } diff --git a/app-package-launcher/Simulator.swift b/app-package-launcher/Simulator.swift index 8317c1e..d7e370f 100644 --- a/app-package-launcher/Simulator.swift +++ b/app-package-launcher/Simulator.swift @@ -1,32 +1,34 @@ +import Foundation + struct Simulator { let identifierString: String var name: String { return identifierString.truncateUuid() } static func allSimulators() -> [Simulator] { - return Shell.run("xcrun instruments -s") + return Shell.run(command: "xcrun instruments -s devices") .filterSimulators() - .sort { $0 > $1 } + .sorted { $0 > $1 } .map { Simulator(identifierString: $0) } } - static func simulatorsMatchingIdentifier(identifier: String) -> [Simulator] { + static func simulatorsMatchingIdentifier(_ identifier: String) -> [Simulator] { let all = allSimulators() guard identifier.characters.count > 0 else { return all } return all.filter { simulator in - return simulator.identifierString.containsString(identifier) + return simulator.identifierString.contains(identifier) } } } -extension CollectionType where Generator.Element == String { +extension Collection where Iterator.Element == String { func filterSimulators() -> [String] { - return filter { $0.containsString("iPhone") || $0.containsString("iPad") } + return filter { $0.contains("iPhone") || $0.contains("iPad") } } } extension String { func truncateUuid() -> String { - let endMinus38 = endIndex.advancedBy(-38) - return substringToIndex(endMinus38) + let result = components(separatedBy: " [") + return result[0] } -} \ No newline at end of file +} diff --git a/app-package-launcher/SimulatorSelectionViewController.swift b/app-package-launcher/SimulatorSelectionViewController.swift index 6f6cd9e..38de0db 100644 --- a/app-package-launcher/SimulatorSelectionViewController.swift +++ b/app-package-launcher/SimulatorSelectionViewController.swift @@ -5,10 +5,10 @@ class SimulatorSelectionWindowController: NSWindowController { @IBOutlet weak var selectionPopUpButton: NSPopUpButton! - var onSelected: (Simulator -> Void)! + var onSelected: ((Simulator) -> Void)! var simulators: [Simulator]! - class func controller(simulators: [Simulator], onSelected: Simulator -> Void) -> SimulatorSelectionWindowController { + class func controller(_ simulators: [Simulator], onSelected: @escaping (Simulator) -> Void) -> SimulatorSelectionWindowController { let controller = SimulatorSelectionWindowController(windowNibName: "SimulatorSelection") controller.simulators = simulators controller.onSelected = onSelected @@ -20,16 +20,16 @@ class SimulatorSelectionWindowController: NSWindowController { selectionPopUpButton.removeAllItems() selectionPopUpButton.menu = NSMenu(title: "Simulators") - selectionPopUpButton.addItemsWithTitles(titles) + selectionPopUpButton.addItems(withTitles: titles) } - @IBAction func launchTapped(sender: NSButton) { + @IBAction func launchTapped(_ sender: NSButton) { close() onSelected(simulators[selectionPopUpButton.indexOfSelectedItem]) } - @IBAction func cancelTapped(sender: NSButton) { - NSApplication.sharedApplication().terminate(sender) + @IBAction func cancelTapped(_ sender: NSButton) { + NSApplication.shared().terminate(sender) } } diff --git a/app-package-launcher/app-package-launcher.xcodeproj/project.pbxproj b/app-package-launcher/app-package-launcher.xcodeproj/project.pbxproj index 57388d4..d389381 100644 --- a/app-package-launcher/app-package-launcher.xcodeproj/project.pbxproj +++ b/app-package-launcher/app-package-launcher.xcodeproj/project.pbxproj @@ -112,11 +112,12 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = ShopKeep; TargetAttributes = { C0E31FE71AC5A3340046EDA0 = { CreatedOnToolsVersion = 6.2; + LastSwiftMigration = 0800; }; }; }; @@ -223,8 +224,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -232,6 +235,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -265,8 +269,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -274,6 +280,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -283,6 +290,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.10; SDKROOT = macosx; SWIFT_OBJC_BRIDGING_HEADER = "Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; name = Release; }; @@ -301,6 +309,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.shopkeep.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -318,6 +327,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.shopkeep.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/app-package-launcher/app-package-launcher.xcodeproj/xcshareddata/xcschemes/app-package-launcher.xcscheme b/app-package-launcher/app-package-launcher.xcodeproj/xcshareddata/xcschemes/app-package-launcher.xcscheme index 485daeb..84a39a5 100644 --- a/app-package-launcher/app-package-launcher.xcodeproj/xcshareddata/xcschemes/app-package-launcher.xcscheme +++ b/app-package-launcher/app-package-launcher.xcodeproj/xcshareddata/xcschemes/app-package-launcher.xcscheme @@ -1,6 +1,6 @@ GBOptionsHelper { @@ -40,17 +40,17 @@ class Arguments: GBSettings { let options = GBOptionsHelper() options.registerSeparator("NEW INSTALLER") - options.registerOption("a".cChar(), long: appFlag, description: ".app for the installer", flags: .RequiredValue) - options.registerOption("d".cChar(), long: deviceFlag, description: "restrict installer to certain simulators, will be matched with --list-devices on launch", flags: .RequiredValue) - options.registerOption("o".cChar(), long: outFlag, description: "output path for the created installer", flags: .RequiredValue) - options.registerOption("f".cChar(), long: freshInstallFlag, description: "every launch of the installer will result in a fresh install of the app", flags: .NoValue) + options.registerOption("a".cChar(), long: appFlag, description: ".app for the installer", flags: GBOptionFlags()) + options.registerOption("d".cChar(), long: deviceFlag, description: "restrict installer to certain simulators, will be matched with --list-devices on launch", flags: GBOptionFlags()) + options.registerOption("o".cChar(), long: outFlag, description: "output path for the created installer", flags: GBOptionFlags()) + options.registerOption("f".cChar(), long: freshInstallFlag, description: "every launch of the installer will result in a fresh install of the app", flags: .noValue) options.registerSeparator("DEVICES") - options.registerOption("l".cChar(), long: listDevicesFlag, description: "list currently available device identifiers", flags: .NoValue) + options.registerOption("l".cChar(), long: listDevicesFlag, description: "list currently available device identifiers", flags: .noValue) options.registerSeparator("HELP") - options.registerOption("h".cChar(), long: helpFlag, description: "print out this help", flags: .NoValue) - options.registerOption("p".cChar(), long: packageLauncherFlag, description: "use a path for app-package-launcher instead of the default in /usr/local/share", flags: [.RequiredValue, .Invisible]) + options.registerOption("h".cChar(), long: helpFlag, description: "print out this help", flags: .noValue) + options.registerOption("p".cChar(), long: packageLauncherFlag, description: "use a path for app-package-launcher instead of the default in /usr/local/share", flags: .invisible) - parser.registerSettings(self) + parser.register(self) parser.registerOptions(options) parser.parseOptionsUsingDefaultArguments() diff --git a/src/Packaging.swift b/src/Packaging.swift index b1aca47..9d35a44 100644 --- a/src/Packaging.swift +++ b/src/Packaging.swift @@ -1,18 +1,18 @@ -enum PackagingError: ErrorType { - case RequiredXcodeUnavailable(String) - case InvalidAppPath(String) - case XcodebuildFailed - case FileWriteFailed(NSError) +enum PackagingError: Error { + case requiredXcodeUnavailable(String) + case invalidAppPath(String) + case xcodebuildFailed + case fileWriteFailed(NSError) var message: String { switch self { - case .RequiredXcodeUnavailable(let requiredVersion): + case .requiredXcodeUnavailable(let requiredVersion): return "You need to have \(requiredVersion) installed and selected via xcode-select." - case .InvalidAppPath(let path): + case .invalidAppPath(let path): return "Provided .app not found at \(path)" - case .XcodebuildFailed: + case .xcodebuildFailed: return "Error in xcodebuild when packaging app" - case .FileWriteFailed(let error): + case .fileWriteFailed(let error): return "Writing output bundle failed: \(error.localizedDescription)" } } @@ -21,46 +21,75 @@ enum PackagingError: ErrorType { class Packaging { static func packageAppAtPath( - appPath: String, + _ appPath: String, deviceIdentifier: String?, outputPath outputPathMaybe: String?, packageLauncherPath packageLauncherPathMaybe: String?, shouldUninstall: Bool, - fileManager: NSFileManager) throws { + fileManager: FileManager) throws { let packageLauncherPath = packageLauncherPathMaybe ?? "/usr/local/share/app-package-launcher" - guard Xcode.isRequiredVersionInstalled() else { throw PackagingError.RequiredXcodeUnavailable(Xcode.requiredVersion) } - guard fileManager.fileExistsAtPath(appPath) else { throw PackagingError.InvalidAppPath(appPath) } - let fullAppPath = NSURL(fileURLWithPath: appPath).path! + guard Xcode.isRequiredVersionInstalled() else { throw PackagingError.requiredXcodeUnavailable(Xcode.requiredVersion) } + guard fileManager.fileExists(atPath: appPath) else { throw PackagingError.invalidAppPath(appPath) } + let fullAppPath = URL(fileURLWithPath: appPath).path let outputPath = outputPathMaybe ?? defaultOutputPathForAppPath(appPath) let productFolder = "\(packageLauncherPath)/build" let productPath = "\(productFolder)/Release/app-package-launcher.app" - let packagedAppFlag = "\"PACKAGED_APP=\(fullAppPath)\"" + let packagedAppFlag = "PACKAGED_APP=\(fullAppPath)" let targetDeviceFlag = deviceIdentifier != nil ? "\"TARGET_DEVICE=\(deviceIdentifier!)\"" : "" let uninstallFlag = shouldUninstall ? "UNINSTALL=1" : "" - let xcodebuildExitCode = - system("xcodebuild -project \(packageLauncherPath)/app-package-launcher.xcodeproj \(packagedAppFlag) \(targetDeviceFlag) \(uninstallFlag) > /dev/null") - guard xcodebuildExitCode == 0 else { throw PackagingError.XcodebuildFailed } + + + let task = ShellTask(launchPath: "/usr/bin/xcodebuild") + task.arguments = [ + "-project", "\(packageLauncherPath)/app-package-launcher.xcodeproj", + packagedAppFlag, + targetDeviceFlag, + uninstallFlag, + ].filter({ $0 != "" }) + task.outputOptions = [ + .print(prefix: "OUT") + ] + task.errorOptions = [ + .print(prefix: "ERR") + ] + + print("command:", task.command) + + task.launch { result in + + switch result { + case .success: + break + case let .failure(code): + print("failed:", code) + } + + } + + task.waitUntilExit() + + guard task.terminationStatus == 0 else { throw PackagingError.xcodebuildFailed } do { - if fileManager.fileExistsAtPath(outputPath) { - try fileManager.removeItemAtPath(outputPath) + if fileManager.fileExists(atPath: outputPath) { + try fileManager.removeItem(atPath: outputPath) } - try fileManager.moveItemAtPath(productPath, toPath: outputPath) - try fileManager.removeItemAtPath(productFolder) + try fileManager.moveItem(atPath: productPath, toPath: outputPath) + try fileManager.removeItem(atPath: productFolder) } catch let error as NSError { - throw PackagingError.FileWriteFailed(error) + throw PackagingError.fileWriteFailed(error) } print("\(appPath) successfully packaged to \(outputPath)") } - static func defaultOutputPathForAppPath(appPath: String) -> String { - let url = NSURL(fileURLWithPath: appPath) - let appName = url.URLByDeletingPathExtension?.lastPathComponent ?? "App" + static func defaultOutputPathForAppPath(_ appPath: String) -> String { + let url = URL(fileURLWithPath: appPath) + let appName = url.deletingPathExtension().lastPathComponent return "\(appName) Installer.app" } diff --git a/src/Shell.swift b/src/Shell.swift index 9ddc87f..bf8126a 100644 --- a/src/Shell.swift +++ b/src/Shell.swift @@ -1,23 +1,296 @@ +import Foundation + class Shell { static func run(command: String) -> [String] { - let task = NSTask() - task.launchPath = "/bin/sh" - task.arguments = ["-c", command] + let task = ShellTask(launchPath: "/bin/sh") + + task.arguments = [ + "-c", + command + ] - let pipe = NSPipe() - task.standardOutput = pipe - task.standardError = pipe + var buffer = Data() + + task.outputOptions = [ + .handle { availableData in + buffer.append(availableData) + } + ] - let file = pipe.fileHandleForReading + task.launch { result in + + switch result { + case .success: + break + case let .failure(code): + print("command [\(task)] failed [\(code)]") + } - task.launch() + } task.waitUntilExit() - let outputData = file.readDataToEndOfFile() - let outputString = NSString(data: outputData, encoding: NSUTF8StringEncoding) as? String - return outputString?.componentsSeparatedByString("\n") ?? [] + let outputString = NSString(data: buffer as Data, encoding: String.Encoding.utf8.rawValue) as? String + return outputString?.components(separatedBy: "\n") ?? [] + } +} + + +// baseed on code from iam Nichols +final class ShellTask { + + enum IOOption { + case print(prefix: String?) + case handle(callback: (_ availableData: Data) -> Void) + } + + enum Result { + case success + case failure(Int32) + } + + let launchPath: String + var arguments: [String]? = nil + var environment: [String: String]? = nil + var currentDirectoryPath: String? = nil + var outputOptions = [IOOption]() + var errorOptions = [IOOption]() + + init(launchPath: String) { + self.launchPath = launchPath + self.taskQueue.name = "ShellTask.LaunchQueue" + } + + deinit { + reset() + } + + fileprivate let taskQueue = OperationQueue() + fileprivate var notificationTokens = [NSObjectProtocol]() + fileprivate var task = Process() + fileprivate var errorPipe = Pipe() + fileprivate var outputPipe = Pipe() + fileprivate var errorReachedEOF = false + fileprivate var outputReachedEOF = false + fileprivate var errorRecievedData = false + fileprivate var outputRecievedData = false + fileprivate var completion: ((_ result: ShellTask.Result) -> Void)? + + var terminationStatus: Int32? = nil + + var command: String { + get { + return launchPath + " " + (arguments?.joined(separator: " "))! + } + } + + fileprivate func reset() { + + for token in notificationTokens { + NotificationCenter.default.removeObserver(token) + } + notificationTokens.removeAll() + + task = Process() + outputPipe = Pipe() + errorPipe = Pipe() + outputReachedEOF = false + errorReachedEOF = false + outputRecievedData = false + errorRecievedData = false + terminationStatus = nil + completion = nil + } + + var isRunning: Bool { + return completion != nil + } + + func launch(_ completion: @escaping (_ result: ShellTask.Result) -> Void) { + + if isRunning { + fatalError("The instance of ShellTask has already launched.") + } + + // print("[ShellTask] Launching:", launchPath, arguments?.joined(separator: " ") ?? "") + + self.completion = completion + + task.launchPath = launchPath + task.arguments = arguments + if let environment = environment { + task.environment = environment + } + task.standardOutput = outputPipe + task.standardError = errorPipe + if let currentDirectoryPath = self.currentDirectoryPath { + task.currentDirectoryPath = currentDirectoryPath + } + + taskQueue.addOperation(BlockOperation { + + self.setupPipe(self.outputPipe) + self.setupPipe(self.errorPipe) + + self.setupTerminationForTask(self.task) + + self.task.launch() + + self.waitUntilExit() + + DispatchQueue.main.async { + self.complete() + } + }) + } + + func waitUntilExit() { + while !self.hasCompleted() { + RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date.distantFuture) + } + } + + fileprivate func complete() { + + if let completion = self.completion, let terminationStatus = self.terminationStatus { + + if terminationStatus == EXIT_SUCCESS { + completion(.success) + } else { + completion(.failure(terminationStatus)) + } + + // self.reset() + + } else { + fatalError("completion or termination status aren't present but the task completed.") + } + } + + fileprivate func hasCompleted() -> Bool { + return outputReachedEOF && errorReachedEOF && terminationStatus != nil + } +} + +// MARK: - File Handles +private extension ShellTask { + + func setupPipe(_ pipe: Pipe) { + + let center = NotificationCenter.default + let name = NSNotification.Name.NSFileHandleDataAvailable + let handle = pipe.fileHandleForReading + let callback = fileHandleDataAvailableBlock + + handle.waitForDataInBackgroundAndNotify() + + let token = center.addObserver(forName: name, object: handle, queue: taskQueue, using: callback) + notificationTokens.append(token) + } + + func fileHandleDataAvailableBlock(_ notification: Notification) { + if let fileHandle = notification.object as? FileHandle { + fileHandleDataAvailable(fileHandle) + } + } + + func fileHandleDataAvailable(_ fileHandle: FileHandle) { + + let availableData = fileHandle.availableData + + if fileHandle === outputPipe.fileHandleForReading { + processData(availableData, withOptions: outputOptions, initialChunk: !outputRecievedData) + } else if fileHandle === errorPipe.fileHandleForReading { + processData(availableData, withOptions: errorOptions, initialChunk: !errorRecievedData) + } + + if availableData.count == 0 { + fileHandleReachedEOF(fileHandle) + } + + if fileHandle === outputPipe.fileHandleForReading && !outputRecievedData { + outputRecievedData = true + } else if fileHandle === errorPipe.fileHandleForReading && !errorRecievedData { + errorRecievedData = true + } + + fileHandle.waitForDataInBackgroundAndNotify() + } + + func fileHandleReachedEOF(_ fileHandle: FileHandle) { + + if fileHandle === outputPipe.fileHandleForReading { + outputReachedEOF = true + } else if fileHandle === errorPipe.fileHandleForReading { + errorReachedEOF = true + } + } +} + +// MARK: - IOOption +private extension ShellTask { + + func processData(_ availableData: Data, withOptions options: [IOOption], initialChunk: Bool) { + + for option in options { + + switch option { + case let .print(prefix): + // print the NSData via `print` + if let chunk = String(data: availableData, encoding: String.Encoding.utf8) { + printChunk(chunk, prefix: prefix, initialChunk: initialChunk, EOF: availableData.count == 0) + } + + case let .handle(callback): + // pass the data back through the closure + callback(availableData) + } + } + } + + func printChunk(_ chunk: String, prefix: String?, initialChunk: Bool, EOF: Bool) { + + if let prefix = prefix , EOF == false { + + var output = chunk + + if initialChunk == true { + output = prefix + " " + output + } + + output = output.replacingOccurrences(of: "\n", with: "\n" + prefix + " ") + + print(output, separator: "", terminator: "") + + } else { + + print(chunk, separator: "", terminator: EOF ? "\n" : "") + } + } +} + +// MARK: - Termination +private extension ShellTask { + + func setupTerminationForTask(_ task: Process) { + + let center = NotificationCenter.default + let name = Process.didTerminateNotification + let callback = taskDidTerminateBlock + + let token = center.addObserver(forName: name, object: task, queue: taskQueue, using: callback) + notificationTokens.append(token) + } + + func taskDidTerminateBlock(_ notification: Notification) { + if let task = notification.object as? Process { + taskDidTerminate(task) + } + } + + func taskDidTerminate(_ task: Process) { + terminationStatus = task.terminationStatus } } diff --git a/src/Simulator.swift b/src/Simulator.swift index eddbbc4..ee18dcc 100644 --- a/src/Simulator.swift +++ b/src/Simulator.swift @@ -2,31 +2,31 @@ struct Simulator { let identifierString: String static func allSimulators() -> [Simulator] { - return Shell.run("xcrun instruments -s") + return Shell.run(command: "xcrun instruments -s") .filterSimulators() .map { $0.truncateUuid() } - .sort { $0 > $1 } + .sorted { $0 > $1 } .map { Simulator(identifierString: $0) } } - static func simulatorsMatchingIdentifier(identifier: String) -> [Simulator] { + static func simulatorsMatchingIdentifier(_ identifier: String) -> [Simulator] { let all = allSimulators() guard identifier.characters.count > 0 else { return all } return all.filter { simulator in - return simulator.identifierString.containsString(identifier) + return simulator.identifierString.contains(identifier) } } } -extension CollectionType where Generator.Element == String { +extension Collection where Iterator.Element == String { func filterSimulators() -> [String] { - return filter { $0.containsString("iPhone") || $0.containsString("iPad") } + return filter { $0.contains("iPhone") || $0.contains("iPad") } } } extension String { func truncateUuid() -> String { - let endMinus38 = endIndex.advancedBy(-38) - return substringToIndex(endMinus38) + let endMinus38 = characters.index(endIndex, offsetBy: -38) + return substring(to: endMinus38) } -} \ No newline at end of file +} diff --git a/src/StringToCChar.swift b/src/StringToCChar.swift index ddcb759..373e652 100644 --- a/src/StringToCChar.swift +++ b/src/StringToCChar.swift @@ -1,5 +1,5 @@ extension String { func cChar() -> CChar { - return cStringUsingEncoding(NSUTF8StringEncoding)!.first! + return cString(using: String.Encoding.utf8)!.first! } -} \ No newline at end of file +} diff --git a/src/Xcode.swift b/src/Xcode.swift index ac0ba59..6ebdcee 100644 --- a/src/Xcode.swift +++ b/src/Xcode.swift @@ -1,6 +1,6 @@ class Xcode { - static let requiredVersion = "Xcode 7" + static let requiredVersion = "Xcode 8" static func isRequiredVersionInstalled() -> Bool { guard let currentVersion = Xcode.currentVersion() else { return false } @@ -8,6 +8,6 @@ class Xcode { } static func currentVersion() -> String? { - return Shell.run("xcodebuild -version").first + return Shell.run(command: "xcodebuild -version").first } } diff --git a/src/ios-simulator-app-installer.xcodeproj/project.pbxproj b/src/ios-simulator-app-installer.xcodeproj/project.pbxproj index c950922..8aba05b 100644 --- a/src/ios-simulator-app-installer.xcodeproj/project.pbxproj +++ b/src/ios-simulator-app-installer.xcodeproj/project.pbxproj @@ -137,11 +137,12 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0700; + LastUpgradeCheck = 0800; ORGANIZATIONNAME = ShopKeep; TargetAttributes = { C07EACC11AC6CD4200E5201D = { CreatedOnToolsVersion = 6.2; + LastSwiftMigration = 0800; }; }; }; @@ -226,8 +227,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -235,6 +238,7 @@ ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -269,8 +273,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; @@ -278,6 +284,7 @@ ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -288,6 +295,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_OBJC_BRIDGING_HEADER = "Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; name = Release; }; @@ -299,6 +307,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "ios-simulator-app-installer"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -309,6 +318,7 @@ CLANG_ENABLE_MODULES = YES; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "ios-simulator-app-installer"; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/src/ios-simulator-app-installer.xcodeproj/xcshareddata/xcschemes/ios-simulator-app-installer.xcscheme b/src/ios-simulator-app-installer.xcodeproj/xcshareddata/xcschemes/ios-simulator-app-installer.xcscheme index 14ed9a3..9109f15 100644 --- a/src/ios-simulator-app-installer.xcodeproj/xcshareddata/xcschemes/ios-simulator-app-installer.xcscheme +++ b/src/ios-simulator-app-installer.xcodeproj/xcshareddata/xcschemes/ios-simulator-app-installer.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/src/main.swift b/src/main.swift index 349de83..ea59518 100644 --- a/src/main.swift +++ b/src/main.swift @@ -1,4 +1,4 @@ -let arguments = Arguments(name: "CommandLine", parent: nil) +let arguments = Arguments(name: "CommandLine", parent: nil)! let options = arguments.parse() if arguments.displayHelp { @@ -17,7 +17,7 @@ if arguments.displayHelp { outputPath: arguments.outputPath, packageLauncherPath: arguments.packageLauncherPath, shouldUninstall: arguments.shouldUninstallApp, - fileManager: NSFileManager.defaultManager() + fileManager: FileManager.default ) } catch let error as PackagingError { print(error.message)