diff --git a/Makefile b/Makefile index 3a5343f9..217373e6 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ #!/usr/bin/xcrun make -f -CARTHAGE_TEMPORARY_FOLDER?=/tmp/Carthage++.dst +CARTHAGE_TEMPORARY_FOLDER?=/tmp/Carthage.dst PREFIX?=/usr/local CONFIGURATION?=release INTERNAL_PACKAGE=CarthageApp.pkg -OUTPUT_PACKAGE=Carthage++.pkg +OUTPUT_PACKAGE=Carthage.pkg CARTHAGE_EXECUTABLE=./.build/$(CONFIGURATION)/carthage BINARIES_FOLDER=/usr/local/bin @@ -58,10 +58,10 @@ installables: package: installables $(MKDIR) "$(CARTHAGE_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" - $(CP) "$(CARTHAGE_EXECUTABLE)" "$(CARTHAGE_TEMPORARY_FOLDER)$(BINARIES_FOLDER)/carthage++" + $(CP) "$(CARTHAGE_EXECUTABLE)" "$(CARTHAGE_TEMPORARY_FOLDER)$(BINARIES_FOLDER)" pkgbuild \ - --identifier "org.carthage.carthage++" \ + --identifier "org.carthage.carthage" \ --install-location "/" \ --root "$(CARTHAGE_TEMPORARY_FOLDER)" \ --version "$(VERSION_STRING)" \ @@ -74,13 +74,13 @@ package: installables prefix_install: installables $(MKDIR) "$(PREFIX)/bin" - $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(PREFIX)/bin/carthage++" + $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(PREFIX)/bin/" install: installables - $(SUDO) $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(BINARIES_FOLDER)/carthage++" + $(SUDO) $(CP) -f "$(CARTHAGE_EXECUTABLE)" "$(BINARIES_FOLDER)" uninstall: - $(RM) "$(BINARIES_FOLDER)/carthage++" + $(RM) "$(BINARIES_FOLDER)/carthage" .build/libSwiftPM.xcconfig: mkdir -p .build diff --git a/Package.resolved b/Package.resolved index f3bb8dd8..2341375b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -12,11 +12,11 @@ }, { "package": "Commandant", - "repositoryURL": "https://github.com/Carthage/Commandant.git", + "repositoryURL": "https://github.com/nsoperations/Commandant.git", "state": { - "branch": null, - "revision": "2cd0210f897fe46c6ce42f52ccfa72b3bbb621a0", - "version": "0.16.0" + "branch": "feature/success-handler", + "revision": "52f74502b2a06a89dffb5c29164fa7a408133225", + "version": null } }, { @@ -28,13 +28,31 @@ "version": "4.0.2" } }, + { + "package": "CwlCatchException", + "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", + "state": { + "branch": null, + "revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66", + "version": "1.2.0" + } + }, + { + "package": "CwlPreconditionTesting", + "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state": { + "branch": null, + "revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b", + "version": "1.2.0" + } + }, { "package": "Nimble", "repositoryURL": "https://github.com/Quick/Nimble.git", "state": { "branch": null, - "revision": "f8657642dfdec9973efc79cc68bcef43a653a2bc", - "version": "8.0.2" + "revision": "6abeb3f5c03beba2b9e4dbe20886e773b5b629b6", + "version": "8.0.4" } }, { @@ -51,8 +69,8 @@ "repositoryURL": "https://github.com/Quick/Quick.git", "state": { "branch": null, - "revision": "94df9b449508344667e5afc7e80f8bcbff1e4c37", - "version": "2.1.0" + "revision": "33682c2f6230c60614861dfc61df267e11a1602f", + "version": "2.2.0" } }, { @@ -64,15 +82,6 @@ "version": "5.0.1" } }, - { - "package": "ReactiveTask", - "repositoryURL": "https://github.com/nsoperations/ReactiveTask.git", - "state": { - "branch": "fix/handle-launch-exceptions", - "revision": "14ef462cf721633eaddd906b70cb7d2015d4ac6c", - "version": null - } - }, { "package": "Result", "repositoryURL": "https://github.com/antitypical/Result.git", diff --git a/Package.swift b/Package.swift index b637813b..6410a2f0 100644 --- a/Package.swift +++ b/Package.swift @@ -10,8 +10,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/antitypical/Result.git", from: "4.1.0"), - .package(url: "https://github.com/nsoperations/ReactiveTask.git", .branch("fix/handle-launch-exceptions")), - .package(url: "https://github.com/Carthage/Commandant.git", .exact("0.16.0")), + .package(url: "https://github.com/nsoperations/Commandant.git", .branch("feature/success-handler")), .package(url: "https://github.com/jdhealy/PrettyColors.git", from: "5.0.2"), .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "5.0.0"), .package(url: "https://github.com/mdiep/Tentacle.git", from: "0.13.1"), @@ -31,7 +30,7 @@ let package = Package( ), .target( name: "CarthageKit", - dependencies: ["XCDBLD", "Tentacle", "Curry", "BTree", "wildmatch"] + dependencies: ["XCDBLD", "Tentacle", "Curry", "BTree", "wildmatch", "ReactiveTask"] ), .testTarget( name: "CarthageKitTests", @@ -43,6 +42,10 @@ let package = Package( dependencies: ["XCDBLD", "CarthageKit", "Commandant", "Curry", "PrettyColors"], exclude: ["swift-is-crashy.c"] ), + .target( + name: "ReactiveTask", + dependencies: ["ReactiveSwift", "Result"] + ), .target( name: "wildmatch" ), diff --git a/Source/CarthageKit/Cache.swift b/Source/CarthageKit/Cache.swift deleted file mode 100644 index 2cbce836..00000000 --- a/Source/CarthageKit/Cache.swift +++ /dev/null @@ -1,230 +0,0 @@ -import Foundation - -/** - This class encapsulates basic dictionary operations with expiring values. This is intentionally implemented as a final class instead of a struct to allow a timer to target it for regular removal. - Also we don't want value semantics for a cache to avoid copying around the values. - - Each stored value has an optional timeToLive which overrides the defaultTimeToLive which is supplied to the initializer. - - This class is implemented in a thread-safe manner and is usable concurrently from multiple threads. - */ -public final class Cache { - - // MARK: - Public properties - - /** - Returns the non-expired entries of this storage as a dictionary. - */ - public var dictionary: [K: V] { - return synchronized(self) { - return storage.reduce(into: [K: V](minimumCapacity: storage.count)) { dict, entry in - if !entry.value.isExpired { - dict[entry.key] = entry.value.value - } - } - } - } - - /** - Returns the size of the cache (including any expired values which have not yet been removed). - */ - public var count: Int { - return synchronized(self) { - return storage.count - } - } - - public let defaultTimeToLive: TimeInterval - - // MARK: - Internal properties - - internal var compactCount = 0 - - // MARK: - Private properties - - private var storage = [K: ExpiringValue]() - private var compactTimer: DispatchSourceTimer? - - // MARK: - Object lifecycle - - /** - Designated initializer which specifies an optional defaultTimeToLive and a compactInterval in case a timer should be scheduled to automatically compact the cache at regular times. - */ - public init(defaultTimeToLive: TimeInterval = TimeInterval.greatestFiniteMagnitude, compactInterval: TimeInterval? = nil) { - self.defaultTimeToLive = defaultTimeToLive - if let interval = compactInterval { - //Triggers in a background queue, so won't disturb the main thread - let timer = DispatchSource.makeTimerSource() - timer.schedule(deadline: .now() + interval, repeating: interval) - timer.setEventHandler { [weak self] in - self?.compact() - } - timer.resume() - compactTimer = timer - } - } - - deinit { - compactTimer?.cancel() - } - - // MARK: - Public methods - - /** - Retrieves the value for the specified key. - - Returns nil if no value exists for this key or if the value has expired. - */ - public subscript(_ key: K) -> V? { - get { - return synchronized(self) { - if let value = storage[key] { - if value.isExpired { - storage.removeValue(forKey: key) - } else { - return value.value - } - } - return nil - } - } - - set { - if let value = newValue { - updateValue(value, forKey: key) - } else { - removeValue(forKey: key) - } - } - } - - /** - Retrieves or sets the value for the specified key with a specified expirationDate. - - The expiration date has no effect on the getter, only the setter. - - Returns nil if no value exists for this key or if the value has expired. - */ - public subscript(_ key: K, expiringAt expirationDate: Date?) -> V? { - get { - return self[key] - } - - set { - if let value = newValue { - updateValue(value, forKey: key, expirationDate: expirationDate) - } else { - removeValue(forKey: key) - } - } - } - - /** - Retrieves or sets the value for the specified key with a specified timeToLive (time interval added to the current date). - - The timeToLive has no effect on the getter, only the setter. - - Returns nil if no value exists for this key or if the value has expired. - */ - public subscript(_ key: K, timeToLive timeToLive: TimeInterval) -> V? { - get { - return self[key] - } - - set { - if let value = newValue { - updateValue(value, forKey: key, expirationDate: Date(timeIntervalSinceNow: timeToLive)) - } else { - removeValue(forKey: key) - } - } - } - - /** - Puts the value for the specified key and returns the old value if present. - - If expirationDate is specified, the stored value will automatically expire after the specified date. If no expiration date is specified the default time to live is used. - */ - @discardableResult - public func updateValue(_ value: V, forKey key: K, expirationDate: Date? = nil) -> V? { - return synchronized(self) { - return storage.updateValue(ExpiringValue(value, expirationDate: expirationDate ?? Date(timeIntervalSinceNow: defaultTimeToLive)), forKey: key).map { $0.value } - } - } - - /** - Removes the value for the specified key. - */ - @discardableResult - public func removeValue(forKey key: K) -> V? { - return synchronized(self) { - if let value = storage.removeValue(forKey: key) { - return value.value - } - return nil - } - } - - /** - Gets the value corresponding with the specified key and, if not found, will invoke the initializer to initialize and set it first. - */ - public func getValue(key: K, default constructor: (K) throws -> V) rethrows -> V { - if let value = self[key] { - return value - } - let value = try constructor(key) - self[key] = value - return value - } - - /** - Removes all stored values. - */ - public func removeAll() { - synchronized(self) { - storage.removeAll() - } - } - - /** - Ensures expired values are removed from the storage. - */ - public func compact() { - synchronized(self) { - storage = storage.filter { !$1.isExpired } - compactCount += 1 - } - } - - // MARK: - ExpiringValue - - private struct ExpiringValue { - let value: V - let expirationDate: Date? - - var isExpired: Bool { - return isExpired(forDate: Date()) - } - - func isExpired(forDate date: Date) -> Bool { - return expirationDate.map { $0 < date } ?? false - } - - init(_ value: V, expirationDate: Date?) { - self.value = value - self.expirationDate = expirationDate - } - } -} - -/** - Swift equivalence of @synchronized in ObjectiveC until a native language alternative comes around. - */ -@discardableResult -private func synchronized(_ object: AnyObject, closure: () throws -> T) rethrows -> T { - objc_sync_enter(object) - defer { - objc_sync_exit(object) - } - return try closure() -} diff --git a/Source/CarthageKit/CarthageKitVersion.swift b/Source/CarthageKit/CarthageKitVersion.swift index fd63a37d..61d1ed3a 100644 --- a/Source/CarthageKit/CarthageKitVersion.swift +++ b/Source/CarthageKit/CarthageKitVersion.swift @@ -1,5 +1,5 @@ /// Defines the current CarthageKit version. public struct CarthageKitVersion { public let value: SemanticVersion - public static let current = CarthageKitVersion(value: SemanticVersion(0, 40, 2, prereleaseIdentifiers: [], buildMetadataIdentifiers: [])) + public static let current = CarthageKitVersion(value: SemanticVersion(0, 40, 2, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["nsoperations"])) } diff --git a/Source/CarthageKit/DebugSymbolsMapper.swift b/Source/CarthageKit/DebugSymbolsMapper.swift index d878dc04..7e19e4eb 100644 --- a/Source/CarthageKit/DebugSymbolsMapper.swift +++ b/Source/CarthageKit/DebugSymbolsMapper.swift @@ -86,8 +86,14 @@ final class DebugSymbolsMapper { private static func uuidsOfDwarf(_ binaryURL: URL) throws -> [String: String] { + // The output of dwarfdump is a series of lines formatted as follows + // for each architecture: + // + // UUID: () + // + let normalizedURL = try normalizedBinaryURL(url: binaryURL) - let task = Task("/usr/bin/xcrun", arguments: ["dwarfdump", "--uuid", normalizedURL.path]) + let task = Task("/usr/bin/xcrun", arguments: ["dwarfdump", "--uuid", normalizedURL.path], useCache: true) let stdOutString = try task.getStdOutString().mapError(CarthageError.taskError).get() @@ -101,7 +107,6 @@ final class DebugSymbolsMapper { archsToUUIDs[elements[2].replacingOccurrences(of: "(", with: "").replacingOccurrences(of: ")", with: "")] = String(elements[1]) } } - return archsToUUIDs } diff --git a/Source/CarthageKit/Frameworks.swift b/Source/CarthageKit/Frameworks.swift index 6769c452..eac6512e 100644 --- a/Source/CarthageKit/Frameworks.swift +++ b/Source/CarthageKit/Frameworks.swift @@ -512,7 +512,7 @@ final class Frameworks { /// Sends a set of UUIDs for each architecture present in the given URL. private static func UUIDsFromDwarfdump(_ url: URL) -> SignalProducer, CarthageError> { - let dwarfdumpTask = Task("/usr/bin/xcrun", arguments: [ "dwarfdump", "--uuid", url.path ]) + let dwarfdumpTask = Task("/usr/bin/xcrun", arguments: [ "dwarfdump", "--uuid", url.path ], useCache: true) return dwarfdumpTask.launch() .ignoreTaskData() diff --git a/Source/CarthageKit/Git.swift b/Source/CarthageKit/Git.swift index b0e6d149..2bf5f9e9 100644 --- a/Source/CarthageKit/Git.swift +++ b/Source/CarthageKit/Git.swift @@ -70,7 +70,8 @@ public final class Git { _ arguments: [String], repositoryFileURL: URL? = nil, standardInput: SignalProducer? = nil, - environment: [String: String]? = nil + environment: [String: String]? = nil, + useCache: Bool = false ) -> SignalProducer { // See https://github.com/Carthage/Carthage/issues/219. var updatedEnvironment = environment ?? ProcessInfo.processInfo.environment @@ -79,7 +80,7 @@ public final class Git { // Error rather than prompt to resolve ssh errors (such as missing known_hosts entry) updatedEnvironment["GIT_SSH_COMMAND"] = "ssh -oBatchMode=yes" - let taskDescription = Task("/usr/bin/env", arguments: [ "git" ] + arguments, workingDirectoryPath: repositoryFileURL?.path, environment: updatedEnvironment) + let taskDescription = Task("/usr/bin/env", arguments: [ "git" ] + arguments, workingDirectoryPath: repositoryFileURL?.path, environment: updatedEnvironment, useCache: useCache) return taskDescription.launch(standardInput: standardInput) .ignoreTaskData() diff --git a/Source/CarthageKit/GitHub.swift b/Source/CarthageKit/GitHub.swift index 4f6095b9..11905079 100644 --- a/Source/CarthageKit/GitHub.swift +++ b/Source/CarthageKit/GitHub.swift @@ -3,17 +3,10 @@ import Result import ReactiveSwift import Tentacle -/// The User-Agent to use for GitHub requests. -private func gitHubUserAgent() -> String { - let identifier = Constants.bundleIdentifier - let version = CarthageKitVersion.current.value - return "\(identifier)/\(version)" -} - extension Server { /// The URL that should be used for cloning the given repository over HTTPS. public func httpsURL(for repository: Repository) -> GitURL { - let auth = tokenFromEnvironment(forServer: self).map { "\($0)@" } ?? "" + let auth = Client.gitHubToken(forServer: self).map { "\($0)@" } ?? "" let scheme = url.scheme! return GitURL("\(scheme)://\(auth)\(url.host!)/\(repository.owner)/\(repository.name).git") @@ -82,84 +75,91 @@ extension Release { } } -private func credentialsFromGit(forServer server: Server) -> (String, String)? { - let data = "url=\(server)".data(using: .utf8)! - - return Git.launchGitTask([ "credential", "fill" ], standardInput: SignalProducer(value: data)) - .flatMap(.concat) { string in - return string.linesProducer - } - .reduce(into: [:]) { (values: inout [String: String], line: String) in - let parts = line - .split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" } - .map(String.init) - - if parts.count >= 2 { - let key = parts[0] - let value = parts[1] - - values[key] = value - } - } - .map { (values: [String: String]) -> (String, String)? in - if let username = values["username"], let password = values["password"] { - return (username, password) - } - - return nil - } - .first()? - .value ?? nil // swiftlint:disable:this redundant_nil_coalescing -} - -private func tokenFromEnvironment(forServer server: Server) -> String? { - let environment = ProcessInfo.processInfo.environment - - if let accessTokenInput = environment["GITHUB_ACCESS_TOKEN"] { - // Treat the input as comma-separated series of domains and tokens. - // (e.g., `GITHUB_ACCESS_TOKEN="github.com=XXXXXXXXXXXXX,enterprise.local/ghe=YYYYYYYYY"`) - let records = accessTokenInput - .split(omittingEmptySubsequences: true) { $0 == "," } - .reduce(into: [:]) { (values: inout [String: String], record) in - let parts = record.split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" }.map(String.init) - switch parts.count { - case 1: - // If the input is provided as an access token itself, use the - // token for Github.com. - values["github.com"] = parts[0] - - case 2: - let (server, token) = (parts[0], parts[1]) - values[server] = token - - default: - break - } - } - return records[server.url.host!] - } - - return nil -} - extension Client { + convenience init(server: Server, isAuthenticated: Bool = true) { if Client.userAgent == nil { - Client.userAgent = gitHubUserAgent() + Client.userAgent = Client.gitHubUserAgent() } let urlSession = URLSession.proxiedSession if !isAuthenticated { self.init(server, urlSession: urlSession) - } else if let token = tokenFromEnvironment(forServer: server) { + } else if let token = Client.gitHubToken(forServer: server) { self.init(server, token: token, urlSession: urlSession) - } else if let (username, password) = credentialsFromGit(forServer: server) { + } else if let (username, password) = Client.credentialsFromGit(forServer: server) { self.init(server, username: username, password: password, urlSession: urlSession) } else { self.init(server, urlSession: urlSession) } } + + private static func credentialsFromGit(forServer server: Server) -> (String, String)? { + let data = "url=\(server)".data(using: .utf8)! + return Git.launchGitTask([ "credential", "fill" ], standardInput: SignalProducer(value: data), useCache: true) + .flatMap(.concat) { string in + return string.linesProducer + } + .reduce(into: [:]) { (values: inout [String: String], line: String) in + let parts = line + .split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" } + .map(String.init) + + if parts.count >= 2 { + let key = parts[0] + let value = parts[1] + + values[key] = value + } + } + .map { (values: [String: String]) -> (String, String)? in + if let username = values["username"], let password = values["password"] { + return (username, password) + } + + return nil + } + .first()? + .value ?? nil // swiftlint:disable:this redundant_nil_coalescing + } + + /// The User-Agent to use for GitHub requests. + fileprivate static func gitHubUserAgent() -> String { + let identifier = Constants.bundleIdentifier + let version = CarthageKitVersion.current.value + return "\(identifier)/\(version)" + } + + fileprivate static func gitHubToken(forServer server: Server) -> String? { + let environment = ProcessInfo.processInfo.environment + + if let accessTokenInput = environment["GITHUB_ACCESS_TOKEN"] { + // Treat the input as comma-separated series of domains and tokens. + // (e.g., `GITHUB_ACCESS_TOKEN="github.com=XXXXXXXXXXXXX,enterprise.local/ghe=YYYYYYYYY"`) + let records = accessTokenInput + .split(omittingEmptySubsequences: true) { $0 == "," } + .reduce(into: [:]) { (values: inout [String: String], record) in + let parts = record.split(maxSplits: 1, omittingEmptySubsequences: true) { $0 == "=" }.map(String.init) + switch parts.count { + case 1: + // If the input is provided as an access token itself, use the + // token for Github.com. + values["github.com"] = parts[0] + + case 2: + let (server, token) = (parts[0], parts[1]) + values[server] = token + + default: + break + } + } + return records[server.url.host!] + } + + return nil + } } extension URLSession { diff --git a/Source/CarthageKit/SwiftToolchain.swift b/Source/CarthageKit/SwiftToolchain.swift index 7d792883..b5a96cfb 100644 --- a/Source/CarthageKit/SwiftToolchain.swift +++ b/Source/CarthageKit/SwiftToolchain.swift @@ -8,8 +8,6 @@ import struct Foundation.URL /// Swift compiler helper methods public final class SwiftToolchain { - private static let swiftVersionCache = Cache>() - internal static var swiftVersionRegex: NSRegularExpression = try! NSRegularExpression(pattern: "Apple Swift version ([^\\s]+) .*\\((.[^\\)]+)\\)", options: []) /// Emits the currect Swift version @@ -55,22 +53,15 @@ public final class SwiftToolchain { /// Attempts to determine the local version of swift private static func determineSwiftVersion(usingToolchain toolchain: String?) -> SignalProducer { - - let result = swiftVersionCache.getValue(key: toolchain) { toolchain in - - let taskDescription = Task("/usr/bin/env", arguments: compilerVersionArguments(usingToolchain: toolchain)) + let taskDescription = Task("/usr/bin/env", arguments: compilerVersionArguments(usingToolchain: toolchain), useCache: true) - return taskDescription.launch(standardInput: nil) - .ignoreTaskData() - .mapError { _ in SwiftVersionError.unknownLocalSwiftVersion } - .map { data -> String? in - return parseSwiftVersionCommand(output: String(data: data, encoding: .utf8)) - } - .attemptMap { Result($0, failWith: SwiftVersionError.unknownLocalSwiftVersion) } - .first()! - } - - return SignalProducer(result: result) + return taskDescription.launch(standardInput: nil) + .ignoreTaskData() + .mapError { _ in SwiftVersionError.unknownLocalSwiftVersion } + .map { data -> String? in + return parseSwiftVersionCommand(output: String(data: data, encoding: .utf8)) + } + .attemptMap { Result($0, failWith: SwiftVersionError.unknownLocalSwiftVersion) } } private static func compilerVersionArguments(usingToolchain toolchain: String?) -> [String] { diff --git a/Source/CarthageKit/VersionFile.swift b/Source/CarthageKit/VersionFile.swift index 0b08ce8b..004ac040 100644 --- a/Source/CarthageKit/VersionFile.swift +++ b/Source/CarthageKit/VersionFile.swift @@ -42,7 +42,7 @@ func &&(lhs: VersionStatus, rhs: VersionStatus) -> VersionStatus { struct VersionFile: Codable { - static let sourceHashCache = Cache() + static let sourceHashCache = Atomic(Dictionary()) enum CodingKeys: String, CodingKey { case commitish = "commitish" @@ -579,6 +579,7 @@ extension VersionFile { # User-specific Xcode files **/xcuserdata/** + **/xcdebugger/** *.xccheckout *.xcscmblueprint diff --git a/Source/CarthageKit/Xcode.swift b/Source/CarthageKit/Xcode.swift index 8cf77b58..05e6021f 100644 --- a/Source/CarthageKit/Xcode.swift +++ b/Source/CarthageKit/Xcode.swift @@ -17,10 +17,6 @@ public typealias SDKFilterCallback = (_ sdks: [SDK], _ scheme: Scheme, _ configu public final class Xcode { - private static let buildSettingsCache = Cache>() - private static let schemeNamesCache = Cache>() - private static let destinationsCache = Cache>() - /// Attempts to build the dependency, then places its build product into the /// root directory given. /// @@ -246,9 +242,9 @@ public final class Xcode { /// Sends each shared scheme name found in the receiver. static func listSchemeNames(project: ProjectLocator) -> SignalProducer { - let schemesResult = schemeNamesCache.getValue(key: project) { project in - let task = xcodebuildTask("-list", BuildArguments(project: project)) - return task.launch() + let task = xcodebuildTask("-list", BuildArguments(project: project), useCache: true) + + return task.launch() .ignoreTaskData() .mapError(CarthageError.taskError) // xcodebuild has a bug where xcodebuild -list can sometimes hang @@ -259,10 +255,6 @@ public final class Xcode { .map { data in return String(data: data, encoding: .utf8)! } - .first()! - } - - return SignalProducer(result: schemesResult) .flatMap(.merge) { string in return string.linesProducer } @@ -296,11 +288,9 @@ public final class Xcode { // // "archive" also works around the issue above so use it to determine if // it is configured for the archive action. - - let commandResult = buildSettingsCache.getValue(key: arguments) { arguments in - let task = xcodebuildTask(["archive", "-showBuildSettings", "-skipUnavailableActions"], arguments) - - return task.launch() + let task = xcodebuildTask(["archive", "-showBuildSettings", "-skipUnavailableActions"], arguments, useCache: true) + + return task.launch() .ignoreTaskData() .mapError(CarthageError.taskError) // xcodebuild has a bug where xcodebuild -showBuildSettings @@ -312,14 +302,8 @@ public final class Xcode { .map { data in return String(data: data, encoding: .utf8)! } - .first()! - } - - switch commandResult { - case let .success(output): - return BuildSettings.produce(string: output, arguments: arguments, action: action) - case let .failure(error): - return SignalProducer(error: error) + .flatMap(.merge) { string -> SignalProducer in + BuildSettings.produce(string: string, arguments: arguments, action: action) } } @@ -365,14 +349,14 @@ public final class Xcode { /// Creates a task description for executing `xcodebuild` with the given /// arguments. - private static func xcodebuildTask(_ tasks: [String], _ buildArguments: BuildArguments) -> Task { - return Task("/usr/bin/xcrun", arguments: buildArguments.arguments + tasks) + private static func xcodebuildTask(_ tasks: [String], _ buildArguments: BuildArguments, workingDirectoryPath: String? = nil, useCache: Bool = false) -> Task { + return Task("/usr/bin/xcrun", arguments: buildArguments.arguments + tasks, workingDirectoryPath: workingDirectoryPath, useCache: useCache) } /// Creates a task description for executing `xcodebuild` with the given /// arguments. - private static func xcodebuildTask(_ task: String, _ buildArguments: BuildArguments) -> Task { - return xcodebuildTask([task], buildArguments) + private static func xcodebuildTask(_ task: String, _ buildArguments: BuildArguments, workingDirectoryPath: String? = nil, useCache: Bool = false) -> Task { + return xcodebuildTask([task], buildArguments, workingDirectoryPath: workingDirectoryPath, useCache: useCache) } /// Sends pairs of a scheme and a project, the scheme actually resides in @@ -833,26 +817,21 @@ public final class Xcode { private static func fetchDestination(sdk: SDK) -> SignalProducer { // Specifying destination seems to be required for building with // simulator SDKs since Xcode 7.2. - - let result = destinationsCache.getValue(key: sdk) { sdk -> Result in - if sdk.isSimulator { - let destinationLookup = Task("/usr/bin/xcrun", arguments: [ "simctl", "list", "devices", "--json" ]) - return destinationLookup.launch() - .mapError(CarthageError.taskError) - .ignoreTaskData() - .flatMap(.concat) { (data: Data) -> SignalProducer in - if let selectedSimulator = Simulator.selectAvailableSimulator(of: sdk, from: data) { - return .init(value: selectedSimulator) - } else { - return .init(error: CarthageError.noAvailableSimulators(platformName: sdk.platform.rawValue)) - } + if sdk.isSimulator { + let destinationLookup = Task("/usr/bin/xcrun", arguments: [ "simctl", "list", "devices", "--json" ], useCache: true) + return destinationLookup.launch() + .mapError(CarthageError.taskError) + .ignoreTaskData() + .flatMap(.concat) { (data: Data) -> SignalProducer in + if let selectedSimulator = Simulator.selectAvailableSimulator(of: sdk, from: data) { + return .init(value: selectedSimulator) + } else { + return .init(error: CarthageError.noAvailableSimulators(platformName: sdk.platform.rawValue)) } - .map { "platform=\(sdk.platform.rawValue) Simulator,id=\($0.udid.uuidString)" } - .first()! - } - return .success(nil) + } + .map { "platform=\(sdk.platform.rawValue) Simulator,id=\($0.udid.uuidString)" } } - return SignalProducer(result: result) + return SignalProducer(value: nil) } /// Runs the build for a given sdk and build arguments, optionally performing a clean first @@ -921,14 +900,16 @@ public final class Xcode { // Disable the "Strip Linked Product" build // setting so we can later generate a dSYM "STRIP_INSTALLED_PRODUCT=NO", + + // Enabled whole module compilation since we are not interested in incremental mode + "SWIFT_COMPILATION_MODE=wholemodule", ] } return result }() - var buildScheme = xcodebuildTask(actions, argsForBuilding) - buildScheme.workingDirectoryPath = workingDirectoryURL.path + let buildScheme = xcodebuildTask(actions, argsForBuilding, workingDirectoryPath: workingDirectoryURL.path) return buildScheme.launch() .flatMapTaskEvents(.concat) { _ in SignalProducer(settings) } .mapError(CarthageError.taskError) diff --git a/Source/ReactiveTask/Cache.swift b/Source/ReactiveTask/Cache.swift new file mode 100644 index 00000000..35cc686c --- /dev/null +++ b/Source/ReactiveTask/Cache.swift @@ -0,0 +1,28 @@ +import Foundation +import ReactiveSwift + +public protocol Cache { + associatedtype Key: Hashable + associatedtype Value + subscript(_ key: Key) -> Value? { get set } +} + +extension Dictionary: Cache { + +} + +extension Atomic where Value: Cache { + public subscript(_ key: Value.Key) -> Value.Value? { + get { + return self.withValue { map -> Value.Value? in + return map[key] + } + } + + set { + self.modify { map in + map[key] = newValue + } + } + } +} diff --git a/Source/ReactiveTask/Task.swift b/Source/ReactiveTask/Task.swift new file mode 100644 index 00000000..81f85fd8 --- /dev/null +++ b/Source/ReactiveTask/Task.swift @@ -0,0 +1,619 @@ +import Foundation +import ReactiveSwift +import Result + +/// Describes how to execute a shell command. +public struct Task { + /// The path to the executable that should be launched. + public let launchPath: String + + /// Any arguments to provide to the executable. + public let arguments: [String] + + /// The path to the working directory in which the process should be + /// launched. + /// + /// If nil, the launched task will inherit the working directory of its + /// parent. + public let workingDirectoryPath: String? + + /// Environment variables to set for the launched process. + /// + /// If nil, the launched task will inherit the environment of its parent. + public let environment: [String: String]? + + public let useCache: Bool + + public init(_ launchPath: String, arguments: [String] = [], workingDirectoryPath: String? = nil, environment: [String: String]? = nil, useCache: Bool = false) { + self.launchPath = launchPath + self.arguments = arguments + self.workingDirectoryPath = workingDirectoryPath + self.environment = environment + self.useCache = useCache + self.identifier = Task.counter.modify { c in + c += 1 + return c + } + } + + private static let counter = Atomic(0) + + public let identifier: Int + + /// A GCD group which to wait completion + fileprivate static let group = DispatchGroup() + + /// wait for all task termination + public static func waitForAllTaskTermination() { + _ = Task.group.wait(timeout: DispatchTime.distantFuture) + } +} + +extension String { + // swiftlint:disable:next force_try + private static let whitespaceRegularExpression = try! NSRegularExpression(pattern: "\\s") + + var escapingWhitespaces: String { + return String.whitespaceRegularExpression.stringByReplacingMatches( + in: self, + range: NSRange(startIndex..., in: self), + withTemplate: "\\\\$0" + ).replacingOccurrences(of: "\0", with: "␀") + } +} + +extension Task: CustomStringConvertible { + public var description: String { + var message = "\(launchPath) \(arguments.map { $0.escapingWhitespaces }.joined(separator: " "))" + if let workingDirectory = workingDirectoryPath { + message += " (launched in \(workingDirectory))" + } + return message + } +} + +extension Task: Hashable { + public static func == (lhs: Task, rhs: Task) -> Bool { + return lhs.launchPath == rhs.launchPath + && lhs.arguments == rhs.arguments + && lhs.workingDirectoryPath == rhs.workingDirectoryPath + && lhs.environment == rhs.environment + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(launchPath) + hasher.combine(arguments) + hasher.combine(workingDirectoryPath) + hasher.combine(environment) + + } +} + +/// A private class used to encapsulate a Unix pipe. +private final class Pipe { + typealias ReadProducer = SignalProducer + + /// The file descriptor for reading data. + let readFD: Int32 + + /// The file descriptor for writing data. + let writeFD: Int32 + + /// A GCD queue upon which to deliver I/O callbacks. + let queue: DispatchQueue + + /// A GCD group which to wait completion + let group: DispatchGroup + + /// Creates an NSFileHandle corresponding to the `readFD`. The file handle + /// will not automatically close the descriptor. + var readHandle: FileHandle { + return FileHandle(fileDescriptor: readFD, closeOnDealloc: false) + } + + /// Creates an NSFileHandle corresponding to the `writeFD`. The file handle + /// will not automatically close the descriptor. + var writeHandle: FileHandle { + return FileHandle(fileDescriptor: writeFD, closeOnDealloc: false) + } + + /// Initializes a pipe object using existing file descriptors. + init(readFD: Int32, writeFD: Int32, queue: DispatchQueue, group: DispatchGroup) { + precondition(readFD >= 0) + precondition(writeFD >= 0) + + self.readFD = readFD + self.writeFD = writeFD + self.queue = queue + self.group = group + } + + /// Instantiates a new descriptor pair. + class func create(_ queue: DispatchQueue, _ group: DispatchGroup) -> Result { + var fildes: [Int32] = [ 0, 0 ] + if pipe(&fildes) == 0 { + return .success(self.init(readFD: fildes[0], writeFD: fildes[1], queue: queue, group: group)) + } else { + return .failure(.posixError(errno)) + } + } + + /// Closes both file descriptors of the receiver. + func closePipe() { + close(readFD) + close(writeFD) + } + + /// Creates a signal that will take ownership of the `readFD` using + /// dispatch_io, then read it to completion. + /// + /// After starting the returned producer, `readFD` should not be used + /// anywhere else, as it may close unexpectedly. + func transferReadsToProducer() -> ReadProducer { + return SignalProducer { observer, lifetime in + self.group.enter() + let channel = DispatchIO(type: .stream, fileDescriptor: self.readFD, queue: self.queue) { error in + if error == 0 { + observer.sendCompleted() + } else if error == ECANCELED { + observer.sendInterrupted() + } else { + observer.send(error: .posixError(error)) + } + + close(self.readFD) + self.group.leave() + } + + channel.setLimit(lowWater: 1) + channel.read(offset: 0, length: Int.max, queue: self.queue) { (done, dispatchData, error) in + if let dispatchData = dispatchData { + // Cast DispatchData to Data. + // See https://gist.github.com/mayoff/6e35e263b9ddd04d9b77e5261212be19. + let nsdata = dispatchData as Any as! NSData // swiftlint:disable:this force_cast + let data = Data(referencing: nsdata) + observer.send(value: data) + } + + if error == ECANCELED { + observer.sendInterrupted() + } else if error != 0 { + observer.send(error: .posixError(error)) + } + + if done { + channel.close() + } + } + + lifetime.observeEnded { + channel.close(flags: .stop) + } + } + } + + /// Creates a dispatch_io channel for writing all data that arrives on + /// `signal` into `writeFD`, then closes `writeFD` when the input signal + /// terminates. + /// + /// After starting the returned producer, `writeFD` should not be used + /// anywhere else, as it may close unexpectedly. + /// + /// Returns a producer that will complete or error. + func writeDataFromProducer(_ producer: SignalProducer) -> SignalProducer<(), TaskError> { + return SignalProducer { observer, lifetime in + self.group.enter() + let channel = DispatchIO(type: .stream, fileDescriptor: self.writeFD, queue: self.queue) { error in + if error == 0 { + observer.sendCompleted() + } else if error == ECANCELED { + observer.sendInterrupted() + } else { + observer.send(error: .posixError(error)) + } + + close(self.writeFD) + self.group.leave() + } + + producer.startWithSignal { signal, producerDisposable in + lifetime += producerDisposable + + signal.observe(Signal.Observer(value: { data in + let dispatchData = data.withUnsafeBytes { (bytes: UnsafePointer) -> DispatchData in + let buffer = UnsafeRawBufferPointer(start: bytes, count: data.count) + return DispatchData(bytes: buffer) + } + + channel.write(offset: 0, data: dispatchData, queue: self.queue) { _, _, error in + if error == ECANCELED { + observer.sendInterrupted() + } else if error != 0 { + observer.send(error: .posixError(error)) + } + } + }, completed: { + channel.close() + }, interrupted: { + observer.sendInterrupted() + })) + } + + lifetime.observeEnded { + channel.close(flags: .stop) + } + } + } +} + +public protocol TaskEventType { + /// The type of value embedded in a `Success` event. + associatedtype T // swiftlint:disable:this type_name + + /// The resulting value, if the event is `Success`. + var value: T? { get } + + /// Maps over the value embedded in a `Success` event. + func map(_ transform: (T) -> U) -> TaskEvent + + /// Convenience operator for mapping TaskEvents to SignalProducers. + func producerMap(_ transform: (T) -> SignalProducer) -> SignalProducer, Error> +} + +/// Represents events that can occur during the execution of a task that is +/// expected to terminate with a result of type T (upon success). +public enum TaskEvent: TaskEventType { + /// The task is about to be launched. + case launch(Task) + + /// Some data arrived from the task on `stdout`. + case standardOutput(Data) + + /// Some data arrived from the task on `stderr`. + case standardError(Data) + + /// The task exited successfully (with status 0), and value T was produced + /// as a result. + case success(T) + + /// The resulting value, if the event is `Success`. + public var value: T? { + if case let .success(value) = self { + return value + } + return nil + } + + /// Maps over the value embedded in a `Success` event. + public func map(_ transform: (T) -> U) -> TaskEvent { + switch self { + case let .launch(task): + return .launch(task) + + case let .standardOutput(data): + return .standardOutput(data) + + case let .standardError(data): + return .standardError(data) + + case let .success(value): + return .success(transform(value)) + } + } + + /// Convenience operator for mapping TaskEvents to SignalProducers. + public func producerMap(_ transform: (T) -> SignalProducer) -> SignalProducer, Error> { + switch self { + case let .launch(task): + return .init(value: .launch(task)) + + case let .standardOutput(data): + return .init(value: .standardOutput(data)) + + case let .standardError(data): + return .init(value: .standardError(data)) + + case let .success(value): + return transform(value).map(TaskEvent.success) + } + } +} + +extension TaskEvent: Equatable where T: Equatable { + public static func == (lhs: TaskEvent, rhs: TaskEvent) -> Bool { + switch (lhs, rhs) { + case let (.launch(left), .launch(right)): + return left == right + + case let (.standardOutput(left), .standardOutput(right)): + return left == right + + case let (.standardError(left), .standardError(right)): + return left == right + + case let (.success(left), .success(right)): + return left == right + + default: + return false + } + } +} + +extension TaskEvent: CustomStringConvertible { + public var description: String { + func dataDescription(_ data: Data) -> String { + return String(data: data, encoding: .utf8) ?? data.description + } + + switch self { + case let .launch(task): + return "launch: \(task)" + + case let .standardOutput(data): + return "stdout: " + dataDescription(data) + + case let .standardError(data): + return "stderr: " + dataDescription(data) + + case let .success(value): + return "success(\(value))" + } + } +} + +extension SignalProducer where Value: TaskEventType { + /// Maps the values inside a stream of TaskEvents into new SignalProducers. + public func flatMapTaskEvents(_ strategy: FlattenStrategy, transform: @escaping (Value.T) -> SignalProducer) -> SignalProducer, Error> { + return self.flatMap(strategy) { taskEvent in + return taskEvent.producerMap(transform) + } + } + + /// Ignores incremental standard output and standard error data from the given + /// task, sending only a single value with the final, aggregated result. + public func ignoreTaskData() -> SignalProducer { + return lift { $0.ignoreTaskData() } + } +} + +extension Signal where Value: TaskEventType { + /// Ignores incremental standard output and standard error data from the given + /// task, sending only a single value with the final, aggregated result. + public func ignoreTaskData() -> Signal { + return self.filterMap { $0.value } + } +} + +extension Task { + + private static let taskCache = Atomic(Dictionary()) + + #if DEBUG + private static let taskHistory = Atomic(Dictionary()) + + public static var history: [Task: TimeInterval] { + return taskHistory.value + } + #endif + + public static var debugLoggingEnabled = false + + /// Launches a new shell task. + /// + /// - Parameters: + /// - standardInput: Data to stream to standard input of the launched process. If nil, stdin will + /// be inherited from the parent process. + /// - shouldBeTerminatedOnParentExit: A flag to control whether the launched child process should be terminated + /// when the parent process exits. The default value is `false`. + /// + /// - Returns: A producer that will launch the receiver when started, then send + /// `TaskEvent`s as execution proceeds. + public func launch( // swiftlint:disable:this function_body_length cyclomatic_complexity + standardInput: SignalProducer? = nil, + shouldBeTerminatedOnParentExit: Bool = false + ) -> SignalProducer, TaskError> { + + if self.useCache, let cachedData = Task.taskCache[self] { + if Task.debugLoggingEnabled { + print("Task #\(self.identifier) cache hit: \(self)") + } + return SignalProducer, TaskError>(values: .launch(self), .standardOutput(cachedData), .success(cachedData)) + } + + #if DEBUG + if Task.debugLoggingEnabled { + if Task.taskHistory[self] != nil { + print("Task #\(self.identifier) has been executed before, consider caching") + } + } + #endif + + var launchDate: Date! + + return SignalProducer { observer, lifetime in + let queue = DispatchQueue(label: self.description, attributes: []) + let group = Task.group + + let process = Process() + process.launchPath = self.launchPath + process.arguments = self.arguments + + if shouldBeTerminatedOnParentExit { + // This is for terminating subprocesses when the parent process exits. + // See https://github.com/Carthage/ReactiveTask/issues/3 for the details. + let selector = Selector(("setStartsNewProcessGroup:")) + if process.responds(to: selector) { + process.perform(selector, with: false as NSNumber) + } + } + + if let cwd = self.workingDirectoryPath { + process.currentDirectoryPath = cwd + } + + if let env = self.environment { + process.environment = env + } + + var stdinProducer: SignalProducer<(), TaskError> = .empty + + if let input = standardInput { + switch Pipe.create(queue, group) { + case let .success(pipe): + process.standardInput = pipe.readHandle + + stdinProducer = pipe.writeDataFromProducer(input).on(started: { + close(pipe.readFD) + }) + + case let .failure(error): + if Task.debugLoggingEnabled { + print("Task #\(self.identifier) failed: \(error)") + } + observer.send(error: error) + return + } + } + + SignalProducer(result: Pipe.create(queue, group).fanout(Pipe.create(queue, group))) + .flatMap(.merge) { stdoutPipe, stderrPipe -> SignalProducer, TaskError> in + let stdoutProducer = stdoutPipe.transferReadsToProducer() + let stderrProducer = stderrPipe.transferReadsToProducer() + + enum Aggregation { + case value(Data) + case failed(TaskError) + case interrupted + + var producer: Pipe.ReadProducer { + switch self { + case let .value(data): + return .init(value: data) + case let .failed(error): + return .init(error: error) + case .interrupted: + return SignalProducer { observer, _ in + observer.sendInterrupted() + } + } + } + } + + return SignalProducer { observer, lifetime in + func startAggregating(producer: Pipe.ReadProducer, chunk: @escaping (Data) -> TaskEvent) -> Pipe.ReadProducer { + let aggregated = MutableProperty(nil) + + producer.startWithSignal { signal, signalDisposable in + lifetime += signalDisposable + + var aggregate = Data() + signal.observe(Signal.Observer(value: { data in + observer.send(value: chunk(data)) + aggregate.append(data) + }, failed: { error in + observer.send(error: error) + aggregated.value = .failed(error) + }, completed: { + aggregated.value = .value(aggregate) + }, interrupted: { + aggregated.value = .interrupted + })) + } + + return aggregated.producer + .skipNil() + .flatMap(.concat) { $0.producer } + } + + let stdoutAggregated = startAggregating(producer: stdoutProducer, chunk: TaskEvent.standardOutput) + let stderrAggregated = startAggregating(producer: stderrProducer, chunk: TaskEvent.standardError) + + process.standardOutput = stdoutPipe.writeHandle + process.standardError = stderrPipe.writeHandle + + group.enter() + process.terminationHandler = { process in + let terminationStatus = process.terminationStatus + let duration = Date().timeIntervalSince(launchDate) + + #if DEBUG + Task.taskHistory[self] = duration + #endif + + if terminationStatus == EXIT_SUCCESS { + // Wait for stderr to finish, then pass + // through stdout. + + if Task.debugLoggingEnabled { + print(String(format: "Task #\(self.identifier) finished successfully in %.2fs.", duration)) + } + + lifetime += stderrAggregated + .then(stdoutAggregated) + .map { data in + if self.useCache { + Task.taskCache[self] = data + } + return TaskEvent.success(data) + } + .start(observer) + } else { + + if Task.debugLoggingEnabled { + print(String(format: "Task #\(self.identifier) failed with exit code \(terminationStatus) in %.2fs.", duration)) + } + // Wait for stdout to finish, then pass + // through stderr. + lifetime += stdoutAggregated + .then(stderrAggregated) + .flatMap(.concat) { data -> SignalProducer, TaskError> in + let errorString = (data.isEmpty ? nil : String(data: data, encoding: .utf8)) + return SignalProducer(error: .shellTaskFailed(self, exitCode: terminationStatus, standardError: errorString)) + } + .start(observer) + } + group.leave() + } + + launchDate = Date() + if Task.debugLoggingEnabled { + print("Task #\(self.identifier) launched: \(self)") + } + + observer.send(value: .launch(self)) + + if #available(macOS 10.13, *) { + do { + defer { + close(stdoutPipe.writeFD) + close(stderrPipe.writeFD) + } + try process.run() + } catch { + if Task.debugLoggingEnabled { + print("Task #\(self.identifier) launch failed: \(error.localizedDescription)") + } + observer.send(error: TaskError.launchFailed(self, reason: error.localizedDescription)) + return + } + } else { + process.launch() + close(stdoutPipe.writeFD) + close(stderrPipe.writeFD) + } + + lifetime += stdinProducer.start() + + lifetime.observeEnded { + process.terminate() + } + } + } + .startWithSignal { signal, taskDisposable in + lifetime += taskDisposable + signal.observe(observer) + } + } + } +} diff --git a/Source/ReactiveTask/TaskError.swift b/Source/ReactiveTask/TaskError.swift new file mode 100644 index 00000000..d99149cc --- /dev/null +++ b/Source/ReactiveTask/TaskError.swift @@ -0,0 +1,37 @@ +import Foundation + +/// An error originating from ReactiveTask. +public enum TaskError: Error, Equatable { + + /// A shell task failed to launch + case launchFailed(Task, reason: String?) + + /// A shell task exited unsuccessfully. + case shellTaskFailed(Task, exitCode: Int32, standardError: String?) + + /// An error was returned from a POSIX API. + case posixError(Int32) +} + +extension TaskError: CustomStringConvertible { + public var description: String { + switch self { + case let .launchFailed(task, reason: reason): + var description = "A shell task (\(task)) failed to launch" + if let reason = reason { + description += ":\n\(reason)" + } + return description + + case let .shellTaskFailed(task, exitCode, standardError): + var description = "A shell task (\(task)) failed with exit code \(exitCode)" + if let standardError = standardError { + description += ":\n\(standardError)" + } + return description + + case let .posixError(code): + return NSError(domain: NSPOSIXErrorDomain, code: Int(code), userInfo: nil).description + } + } +} diff --git a/Source/carthage/Archive.swift b/Source/carthage/Archive.swift index 1ec455fa..1a24d588 100644 --- a/Source/carthage/Archive.swift +++ b/Source/carthage/Archive.swift @@ -52,9 +52,9 @@ public struct ArchiveCommand: CommandProtocol { let frameworkNames = options.frameworkNames let directoryURL = URL(fileURLWithPath: options.directoryPath) return Archive.archiveFrameworks(frameworkNames: frameworkNames, dependencyName: nil, directoryURL: directoryURL, customOutputPath: options.outputPath, frameworkFoundHandler: { path in - carthage.println(formatting.bullets + "Found " + formatting.path(path)) + carthage.printOut(formatting.bullets + "Found " + formatting.path(path)) }).on(value: { outputURL in - carthage.println(formatting.bullets + "Created " + formatting.path(outputURL.path)) + carthage.printOut(formatting.bullets + "Created " + formatting.path(outputURL.path)) }) } } diff --git a/Source/carthage/Bootstrap.swift b/Source/carthage/Bootstrap.swift index ff326e38..02edc632 100644 --- a/Source/carthage/Bootstrap.swift +++ b/Source/carthage/Bootstrap.swift @@ -16,7 +16,7 @@ public struct BootstrapCommand: CommandProtocol { .flatMap(.merge) { project -> SignalProducer<(), CarthageError> in if !FileManager.default.fileExists(atPath: project.resolvedCartfileURL.path) { let formatting = options.checkoutOptions.colorOptions.formatting - carthage.println(formatting.bullets + "No Cartfile.resolved found, updating dependencies") + carthage.printOut(formatting.bullets + "No Cartfile.resolved found, updating dependencies") return project.updateDependencies( shouldCheckout: options.checkoutAfterUpdate, buildOptions: options.buildOptions).then(options.buildProducer(project: project)) diff --git a/Source/carthage/Build.swift b/Source/carthage/Build.swift index b13ecdf4..81a241da 100644 --- a/Source/carthage/Build.swift +++ b/Source/carthage/Build.swift @@ -141,7 +141,7 @@ public struct BuildCommand: CommandProtocol { .on( started: { if let path = temporaryURL?.path { - carthage.println(formatting.bullets + "xcodebuild output can be found in " + formatting.path(path)) + carthage.printOut(formatting.bullets + "xcodebuild output can be found in " + formatting.path(path)) } }, value: { taskEvent in @@ -156,7 +156,7 @@ public struct BuildCommand: CommandProtocol { stderrHandle.write(data) case let .success(project, scheme): - carthage.println(formatting.bullets + "Building scheme " + formatting.quote(scheme.name) + " in " + formatting.projectName(project.description)) + carthage.printOut(formatting.bullets + "Building scheme " + formatting.quote(scheme.name) + " in " + formatting.projectName(project.description)) } } ) diff --git a/Source/carthage/CopyFrameworks.swift b/Source/carthage/CopyFrameworks.swift index f6cfba07..3e4852ae 100644 --- a/Source/carthage/CopyFrameworks.swift +++ b/Source/carthage/CopyFrameworks.swift @@ -31,7 +31,7 @@ public struct CopyFrameworksCommand: CommandProtocol { let symbolsFolder: URL = try self.appropriateDestinationFolder().get() let waitHandler: (URL) -> Void = { url in - carthage.println("Waiting for lock on url: \(url)") + carthage.printOut("Waiting for lock on url: \(url)") } return inputFiles(options) @@ -43,11 +43,11 @@ public struct CopyFrameworksCommand: CommandProtocol { .on(value: { event in switch event { case .copied(let frameworkName): - carthage.println("Copied \(frameworkName)") + carthage.printOut("Copied \(frameworkName)") case .ignored(let frameworkName): - carthage.println("Ignoring \(frameworkName) because it does not support the current architecture") + carthage.printOut("Ignoring \(frameworkName) because it does not support the current architecture") case .skipped(let frameworkName): - carthage.println("Skipped \(frameworkName) because it was already copied before") + carthage.printOut("Skipped \(frameworkName) because it was already copied before") } }) .waitOnCommand() @@ -165,7 +165,7 @@ public struct CopyFrameworksCommand: CommandProtocol { .on( value: { url in let name = url.lastPathComponent - carthage.println("Automatically discovered framework \(name) at: \"\(url.path)\"") + carthage.printOut("Automatically discovered framework \(name) at: \"\(url.path)\"") } ) ) diff --git a/Source/carthage/Diagnose.swift b/Source/carthage/Diagnose.swift index 712acd3f..fdcc9acd 100644 --- a/Source/carthage/Diagnose.swift +++ b/Source/carthage/Diagnose.swift @@ -73,12 +73,12 @@ public struct DiagnoseCommand: CommandProtocol { let repositoryUrl = baseUrl.appendingPathComponent("Repository") - carthage.println(formatting.bullets + "Started storing diagnosis info into directory: \(baseUrl.path)") + carthage.printOut(formatting.bullets + "Started storing diagnosis info into directory: \(baseUrl.path)") let repository = LocalDependencyStore(directoryURL: repositoryUrl) var dependencyMappings: [Dependency: Dependency]? if let mappingsFilePath = options.mappingsFilePath { - carthage.println(formatting.bullets + "Using dependency mappings from file: \(mappingsFilePath)") + carthage.printOut(formatting.bullets + "Using dependency mappings from file: \(mappingsFilePath)") dependencyMappings = try self.mappings(from: mappingsFilePath) } let logger = ResolverEventLogger(colorOptions: options.colorOptions, verbose: options.isVerbose) @@ -94,8 +94,8 @@ public struct DiagnoseCommand: CommandProtocol { } return cartfileURL }.map { cartfileURL -> URL in - carthage.println(formatting.bullets + "Finished storing diagnosis info into directory: \(baseUrl.path)") - carthage.println(formatting.bullets + "Please submit the contents of this directory to the Carthage team for review") + carthage.printOut(formatting.bullets + "Finished storing diagnosis info into directory: \(baseUrl.path)") + carthage.printOut(formatting.bullets + "Please submit the contents of this directory to the Carthage team for review") return cartfileURL } } catch let error { diff --git a/Source/carthage/Extensions.swift b/Source/carthage/Extensions.swift index db375638..42511fc5 100644 --- a/Source/carthage/Extensions.swift +++ b/Source/carthage/Extensions.swift @@ -20,23 +20,15 @@ private let outputQueue = { () -> DispatchQueue in }() /// A thread-safe version of Swift's standard println(). -internal func println() { +internal func printOut(_ object: Any) { outputQueue.async { - Swift.print() + fputs(String(describing: object) + "\n", stdout) } } -/// A thread-safe version of Swift's standard println(). -internal func println(_ object: T) { - outputQueue.async { - Swift.print(object) - } -} - -/// A thread-safe version of Swift's standard print(). -internal func print(_ object: T) { +internal func printErr(_ object: Any) { outputQueue.async { - Swift.print(object, terminator: "") + fputs(String(describing: object) + "\n", stderr) } } diff --git a/Source/carthage/Logger.swift b/Source/carthage/Logger.swift index 82b3056d..5923c271 100644 --- a/Source/carthage/Logger.swift +++ b/Source/carthage/Logger.swift @@ -18,32 +18,32 @@ final class ProjectEventLogger { switch event { case let .cloning(dependency): - carthage.println(formatting.bullets + "Cloning " + formatting.projectName(dependency.name)) + carthage.printOut(formatting.bullets + "Cloning " + formatting.projectName(dependency.name)) case let .fetching(dependency): - carthage.println(formatting.bullets + "Fetching " + formatting.projectName(dependency.name)) + carthage.printOut(formatting.bullets + "Fetching " + formatting.projectName(dependency.name)) case let .checkingOut(dependency, revision): - carthage.println(formatting.bullets + "Checking out " + formatting.projectName(dependency.name) + " at " + formatting.quote(revision)) + carthage.printOut(formatting.bullets + "Checking out " + formatting.projectName(dependency.name) + " at " + formatting.quote(revision)) case let .downloadingBinaryFrameworkDefinition(dependency, url): - carthage.println(formatting.bullets + "Downloading binary-only dependency " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Downloading binary-only dependency " + formatting.projectName(dependency.name) + " at " + formatting.quote(url.absoluteString)) case let .downloadingBinaries(dependency, release): - carthage.println(formatting.bullets + "Downloading " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Downloading " + formatting.projectName(dependency.name) + " binary at " + formatting.quote(release)) case let .skippedDownloadingBinaries(dependency, message): - carthage.println(formatting.bullets + "Skipped downloading " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Skipped downloading " + formatting.projectName(dependency.name) + " binary due to the error:\n\t" + formatting.quote(message)) case let .installingBinaries(dependency, release): - carthage.println(formatting.bullets + "Installing " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Installing " + formatting.projectName(dependency.name) + " binary at " + formatting.quote(release)) case let .storingBinaries(dependency, release): - carthage.println(formatting.bullets + "Storing " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Storing " + formatting.projectName(dependency.name) + " binary at " + formatting.quote(release)) case let .skippedInstallingBinaries(dependency, error): @@ -52,31 +52,31 @@ final class ProjectEventLogger { \(error.map { formatting.quote(String(describing: $0)) } ?? "No matching binary found") Falling back to building from the source """ - carthage.println(output) + carthage.printOut(output) case let .skippedBuilding(dependency, message): - carthage.println(formatting.bullets + "Skipped building " + formatting.projectName(dependency.name) + " due to the error:\n" + message) + carthage.printOut(formatting.bullets + "Skipped building " + formatting.projectName(dependency.name) + " due to the error:\n" + message) case let .skippedBuildingCached(dependency): - carthage.println(formatting.bullets + "Valid cache found for " + formatting.projectName(dependency.name) + ", skipping build") + carthage.printOut(formatting.bullets + "Valid cache found for " + formatting.projectName(dependency.name) + ", skipping build") case let .rebuildingCached(dependency, versionStatus): - carthage.println(formatting.bullets + "Invalid cache found for " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Invalid cache found for " + formatting.projectName(dependency.name) + " because \(versionStatus.humanReadableDescription), rebuilding with all downstream dependencies") case let .buildingUncached(dependency): - carthage.println(formatting.bullets + "No cache found for " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "No cache found for " + formatting.projectName(dependency.name) + ", building with all downstream dependencies") case let .rebuildingBinary(dependency, versionStatus): - carthage.println(formatting.bullets + "Invalid binary found for " + formatting.projectName(dependency.name) + carthage.printOut(formatting.bullets + "Invalid binary found for " + formatting.projectName(dependency.name) + " because \(versionStatus.humanReadableDescription), rebuilding with all downstream dependencies") case let .waiting(url): - carthage.println(formatting.bullets + "Waiting for lock on " + url.path) + carthage.printOut(formatting.bullets + "Waiting for lock on " + url.path) case let .warning(message): - carthage.println(formatting.bullets + "Warning: " + message) + carthage.printOut(formatting.bullets + "Warning: " + message) } } } @@ -94,19 +94,19 @@ final class ResolverEventLogger { switch event { case .foundVersions(let versions, let dependency, let versionSpecifier): if isVerbose { - carthage.println("Versions for dependency '\(dependency)' compatible with versionSpecifier \(versionSpecifier): \(versions)") + carthage.printOut("Versions for dependency '\(dependency)' compatible with versionSpecifier \(versionSpecifier): \(versions)") } case .foundTransitiveDependencies(let transitiveDependencies, let dependency, let version): if isVerbose { - carthage.println("Dependencies for dependency '\(dependency)' with version \(version): \(transitiveDependencies)") + carthage.printOut("Dependencies for dependency '\(dependency)' with version \(version): \(transitiveDependencies)") } case .failedRetrievingTransitiveDependencies(let error, let dependency, let version): - carthage.println("Caught error while retrieving dependencies for \(dependency) at version \(version): \(error)") + carthage.printOut("Caught error while retrieving dependencies for \(dependency) at version \(version): \(error)") case .failedRetrievingVersions(let error, let dependency, _): - carthage.println("Caught error while retrieving versions for \(dependency): \(error)") + carthage.printOut("Caught error while retrieving versions for \(dependency): \(error)") case .rejected(let dependencySet, let error): if isVerbose { - carthage.println("Rejected dependency set:\n\(dependencySet)\n\nReason: \(error)\n") + carthage.printOut("Rejected dependency set:\n\(dependencySet)\n\nReason: \(error)\n") } } } diff --git a/Source/carthage/Outdated.swift b/Source/carthage/Outdated.swift index eb399455..f8925fd5 100644 --- a/Source/carthage/Outdated.swift +++ b/Source/carthage/Outdated.swift @@ -95,20 +95,20 @@ public struct OutdatedCommand: CommandProtocol { let formatting = options.colorOptions.formatting if !outdatedDependencies.isEmpty { - carthage.println(formatting.path("The following dependencies are outdated:")) + carthage.printOut(formatting.path("The following dependencies are outdated:")) for (project, current, applicable, latest) in outdatedDependencies { if options.outputXcodeWarnings { - carthage.println("warning: \(formatting.projectName(project.name)) is out of date (\(current) -> \(applicable)) (Latest: \(latest))") + carthage.printOut("warning: \(formatting.projectName(project.name)) is out of date (\(current) -> \(applicable)) (Latest: \(latest))") } else { let update = UpdateType(currentVersion: current, applicableVersion: applicable, latestVersion: latest) let style = formatting[update] let versionSummary = "\(style(current.description)) -> \(style(applicable.description)) (Latest: \(latest))" - carthage.println(formatting.projectName(project.name) + " " + versionSummary) + carthage.printOut(formatting.projectName(project.name) + " " + versionSummary) } } } else { - carthage.println("All dependencies are up to date.") + carthage.printOut("All dependencies are up to date.") } }) .waitOnCommand() diff --git a/Source/carthage/RemoteVersion.swift b/Source/carthage/RemoteVersion.swift index 2eebea7a..19ae63f9 100644 --- a/Source/carthage/RemoteVersion.swift +++ b/Source/carthage/RemoteVersion.swift @@ -8,7 +8,7 @@ import Tentacle /// The latest version of Carthage as a `Version`. public func remoteVersion() -> SemanticVersion? { let remoteVersionProducer = Client(.dotCom, urlSession: URLSession.proxiedSession) - .execute(Repository(owner: "Carthage", name: "Carthage").releases, perPage: 2) + .execute(Repository(owner: "nsoperations", name: "Carthage").releases, perPage: 2) .mapError(CarthageError.gitHubAPIRequestFailed) .filterMap { _, releases in return releases.first { !$0.isDraft } diff --git a/Source/carthage/SwiftVersion.swift b/Source/carthage/SwiftVersion.swift index 61faf974..c639aff3 100644 --- a/Source/carthage/SwiftVersion.swift +++ b/Source/carthage/SwiftVersion.swift @@ -26,7 +26,7 @@ public struct SwiftVersionCommand: CommandProtocol { public func run(_ options: Options) -> Result<(), CarthageError> { switch SwiftToolchain.swiftVersion(usingToolchain: options.toolchain).first()! { case .success(let pinnedVersion): - carthage.println(pinnedVersion) + carthage.printOut(pinnedVersion) return .success(()) case .failure(let error): let carthageError = CarthageError.internalError(description: error.description) diff --git a/Source/carthage/Validate.swift b/Source/carthage/Validate.swift index 4686685c..c82ea19f 100644 --- a/Source/carthage/Validate.swift +++ b/Source/carthage/Validate.swift @@ -14,7 +14,7 @@ public struct ValidateCommand: CommandProtocol { return project.validate() } .on(value: { _ in - carthage.println("No incompatibilities found in Cartfile.resolved") + carthage.printOut("No incompatibilities found in Cartfile.resolved") }) .waitOnCommand() } diff --git a/Source/carthage/Version.swift b/Source/carthage/Version.swift index ce2f96d1..a91aef04 100644 --- a/Source/carthage/Version.swift +++ b/Source/carthage/Version.swift @@ -9,7 +9,7 @@ public struct VersionCommand: CommandProtocol { public let function = "Display the current version of Carthage" public func run(_ options: NoOptions) -> Result<(), CarthageError> { - carthage.println(CarthageKitVersion.current.value) + carthage.printOut(CarthageKitVersion.current.value) return .success(()) } } diff --git a/Source/carthage/main.swift b/Source/carthage/main.swift index ff21b5d8..325ab6a5 100644 --- a/Source/carthage/main.swift +++ b/Source/carthage/main.swift @@ -8,12 +8,12 @@ import Result setlinebuf(stdout) guard Git.ensureGitVersion().first()?.value == true else { - fputs("Carthage requires git \(Git.carthageRequiredGitVersion) or later.\n", stderr) + printErr("Carthage requires git \(Git.carthageRequiredGitVersion) or later.\n") exit(EXIT_FAILURE) } if let remoteVersion = remoteVersion(), CarthageKitVersion.current.value < remoteVersion { - fputs("Please update to the latest Carthage version: \(remoteVersion). You currently are on \(CarthageKitVersion.current.value)" + "\n", stderr) + printErr("Please update to the latest Carthage version: \(remoteVersion). You currently are on \(CarthageKitVersion.current.value)" + "\n") } if let carthagePath = Bundle.main.executablePath { @@ -34,9 +34,47 @@ registry.register(VersionCommand()) registry.register(DiagnoseCommand()) registry.register(SwiftVersionCommand()) +#if DEBUG +Task.debugLoggingEnabled = true +let start = Date() +#endif + let helpCommand = HelpCommand(registry: registry) registry.register(helpCommand) -registry.main(defaultVerb: helpCommand.verb) { error in - fputs(error.description + "\n", stderr) -} +registry.main(defaultVerb: helpCommand.verb, successHandler: { + + #if DEBUG + let totalDuration = Date().timeIntervalSince(start) + + printErr("-------------------------------------------------------------------------------") + printErr("") + printErr(String(format: "Total duration: %.2fs.", totalDuration)) + printErr("") + + let taskHistory: [Task: TimeInterval] = Task.history + + let totalTaskDuration: TimeInterval = taskHistory.values.reduce(0.0) { $0 + $1 } + + printErr(String(format: "Total duration of tasks: %.2fs.", totalTaskDuration)) + + printErr("") + printErr("Ordered tasks by duration:") + printErr("") + + let orderedTasks: [(key: Task, value: TimeInterval)] = taskHistory.sorted { + $0.value > $1.value + } + + for entry in orderedTasks { + printErr(String(format: "Task #\(entry.key.identifier) took %.2fs: \(entry.key)", entry.value)) + } + + printErr("") + printErr("-------------------------------------------------------------------------------") + + #endif + +}, errorHandler: { error in + printErr(error.description + "\n") +})