| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- /*
- * Copyright 2019 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import Foundation
- /// Convenience function for calling functions in the Shell. This should be used sparingly and only
- /// when interacting with tools that can't be accessed directly in Swift (i.e. CocoaPods,
- /// xcodebuild, etc). Intentionally empty, this enum is used as a namespace.
- public enum Shell {}
- public extension Shell {
- /// A type to represent the result of running a shell command.
- enum Result {
- /// The command was successfully run (based on the output code), with the output string as the
- /// associated value.
- case success(output: String)
- /// The command failed with a given exit code and output.
- case error(code: Int32, output: String)
- }
- /// Log without executing the shell commands.
- static var logOnly = false
- static func setLogOnly() {
- logOnly = true
- }
- /// Execute a command in the user's shell. This creates a temporary shell script and runs the
- /// command from there, instead of calling via `Process()` directly in order to include the
- /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt
- /// other commands.
- ///
- /// - Parameters:
- /// - command: The command to run in the shell.
- /// - outputToConsole: A flag if the command output should be written to the console as well.
- /// - workingDir: An optional working directory to run the shell command in.
- /// - Returns: A Result containing output information from the command.
- static func executeCommandFromScript(_ command: String,
- outputToConsole: Bool = true,
- workingDir: URL? = nil) -> Result {
- let scriptPath: URL
- do {
- let tempScriptsDir = FileManager.default.temporaryDirectory(withName: "temp_scripts")
- try FileManager.default.createDirectory(at: tempScriptsDir,
- withIntermediateDirectories: true,
- attributes: nil)
- scriptPath = tempScriptsDir.appendingPathComponent("wrapper.sh")
- // Write the temporary script contents to the script's path. CocoaPods complains when LANG
- // isn't set in the environment, so explicitly set it here. The `/usr/local/git/current/bin`
- // is to allow the `sso` protocol if it's there.
- let contents = """
- export PATH="/usr/local/bin:/usr/local/git/current/bin:$PATH"
- export LANG="en_US.UTF-8"
- \(command)
- """
- try contents.write(to: scriptPath, atomically: true, encoding: .utf8)
- } catch let FileManager.FileError.failedToCreateDirectory(path, error) {
- fatalError("Could not execute shell command: \(command) - could not create temporary " +
- "script directory at \(path). \(error)")
- } catch {
- fatalError("Could not execute shell command: \(command) - unexpected error. \(error)")
- }
- // Remove the temporary script at the end of this function. If it fails, it's not a big deal
- // since it will be over-written next time and won't affect the Zip file, so we can ignore
- // any failure.
- defer { try? FileManager.default.removeItem(at: scriptPath) }
- // Let the process call directly into the temporary shell script we created.
- let task = Process()
- task.arguments = [scriptPath.path]
- if #available(OSX 10.13, *) {
- if let workingDir = workingDir {
- task.currentDirectoryURL = workingDir
- }
- // Explicitly use `/bin/bash`. Investigate whether or not we can use `/usr/local/env`
- task.executableURL = URL(fileURLWithPath: "/bin/bash")
- } else {
- // Assign the old `currentDirectoryPath` property if `currentDirectoryURL` isn't available.
- if let workingDir = workingDir {
- task.currentDirectoryPath = workingDir.path
- }
- task.launchPath = "/bin/bash"
- }
- // Assign a pipe to read as soon as data is available, log it to the console if requested, but
- // also keep an array of the output in memory so we can pass it back to functions.
- // Assign a pipe to grab the output, and handle it differently if we want to stream the results
- // to the console or not.
- let pipe = Pipe()
- task.standardOutput = pipe
- let outHandle = pipe.fileHandleForReading
- var output: [String] = []
- // If we want to output to the console, create a readabilityHandler and save each line along the
- // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
- // commands (such as any xcodebuild) can run much, much faster.
- if outputToConsole {
- outHandle.readabilityHandler = { pipe in
- // This will be run any time data is sent to the pipe. We want to print it and store it for
- // later. Ignore any non-valid Strings.
- guard let line = String(data: pipe.availableData, encoding: .utf8) else {
- print("Could not get data from pipe for command \(command): \(pipe.availableData)")
- return
- }
- if line != "" {
- output.append(line)
- }
- print(line)
- }
- // Also set the termination handler on the task in order to stop the readabilityHandler from
- // parsing any more data from the task.
- task.terminationHandler = { t in
- guard let stdOut = t.standardOutput as? Pipe else { return }
- stdOut.fileHandleForReading.readabilityHandler = nil
- }
- }
- // Launch the task and wait for it to exit. This will trigger the above readabilityHandler
- // method and will redirect the command output back to the console for quick feedback.
- if outputToConsole {
- print("Running command: \(command).")
- print("----------------- COMMAND OUTPUT -----------------")
- }
- task.launch()
- // If we are not outputting to the console, there is a possibility that
- // the output pipe gets filled (e.g. when running a command that generates
- // lots of output). In this scenario, the process will hang and
- // `task.waitUntilExit()` will never return. To work around this issue,
- // calling `outHandle.readDataToEndOfFile()` before `task.waitUntilExit()`
- // will read from the pipe until the process ends.
- var outData: Data!
- if !outputToConsole {
- outData = outHandle.readDataToEndOfFile()
- }
- task.waitUntilExit()
- if outputToConsole { print("----------------- END COMMAND OUTPUT -----------------") }
- let fullOutput: String
- if outputToConsole {
- fullOutput = output.joined(separator: "\n")
- } else {
- // Force unwrapping since we know it's UTF8 coming from the console.
- fullOutput = String(data: outData, encoding: .utf8)!
- }
- // Check if the task succeeded or not, and return the failure code if it didn't.
- guard task.terminationStatus == 0 else {
- return Result.error(code: task.terminationStatus, output: fullOutput)
- }
- // The command was successful, return the output.
- return Result.success(output: fullOutput)
- }
- /// Execute a command in the user's shell. This creates a temporary shell script and runs the
- /// command from there, instead of calling via `Process()` directly in order to include the
- /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt
- /// other commands.
- ///
- /// This is a variation of `executeCommandFromScript` that also does error handling internally.
- ///
- /// - Parameters:
- /// - command: The command to run in the shell.
- /// - outputToConsole: A flag if the command output should be written to the console as well.
- /// - workingDir: An optional working directory to run the shell command in.
- /// - Returns: A Result containing output information from the command.
- static func executeCommand(_ command: String,
- outputToConsole: Bool = true,
- workingDir: URL? = nil) {
- if logOnly {
- print(command)
- return
- }
- let result = Shell.executeCommandFromScript(command, workingDir: workingDir)
- switch result {
- case let .error(code, output):
- fatalError("""
- `\(command)` failed with exit code \(code) while trying to install pods:
- Output from `\(command)`:
- \(output)
- """)
- case let .success(output):
- // Print the output to the console and return the information for all installed pods.
- print(output)
- }
- }
- }
|