ShellUtils.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. /// Convenience function for calling functions in the Shell. This should be used sparingly and only
  18. /// when interacting with tools that can't be accessed directly in Swift (i.e. CocoaPods,
  19. /// xcodebuild, etc). Intentionally empty, this enum is used as a namespace.
  20. public enum Shell {}
  21. public extension Shell {
  22. /// A type to represent the result of running a shell command.
  23. enum Result {
  24. /// The command was successfully run (based on the output code), with the output string as the
  25. /// associated value.
  26. case success(output: String)
  27. /// The command failed with a given exit code and output.
  28. case error(code: Int32, output: String)
  29. }
  30. /// Log without executing the shell commands.
  31. static var logOnly = false
  32. static func setLogOnly() {
  33. logOnly = true
  34. }
  35. /// Execute a command in the user's shell. This creates a temporary shell script and runs the
  36. /// command from there, instead of calling via `Process()` directly in order to include the
  37. /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt
  38. /// other commands.
  39. ///
  40. /// - Parameters:
  41. /// - command: The command to run in the shell.
  42. /// - outputToConsole: A flag if the command output should be written to the console as well.
  43. /// - workingDir: An optional working directory to run the shell command in.
  44. /// - Returns: A Result containing output information from the command.
  45. static func executeCommandFromScript(_ command: String,
  46. outputToConsole: Bool = true,
  47. workingDir: URL? = nil) -> Result {
  48. let scriptPath: URL
  49. do {
  50. let tempScriptsDir = FileManager.default.temporaryDirectory(withName: "temp_scripts")
  51. try FileManager.default.createDirectory(at: tempScriptsDir,
  52. withIntermediateDirectories: true,
  53. attributes: nil)
  54. scriptPath = tempScriptsDir.appendingPathComponent("wrapper.sh")
  55. // Write the temporary script contents to the script's path. CocoaPods complains when LANG
  56. // isn't set in the environment, so explicitly set it here. The `/usr/local/git/current/bin`
  57. // is to allow the `sso` protocol if it's there.
  58. let contents = """
  59. export PATH="/usr/local/bin:/usr/local/git/current/bin:$PATH"
  60. export LANG="en_US.UTF-8"
  61. \(command)
  62. """
  63. try contents.write(to: scriptPath, atomically: true, encoding: .utf8)
  64. } catch let FileManager.FileError.failedToCreateDirectory(path, error) {
  65. fatalError("Could not execute shell command: \(command) - could not create temporary " +
  66. "script directory at \(path). \(error)")
  67. } catch {
  68. fatalError("Could not execute shell command: \(command) - unexpected error. \(error)")
  69. }
  70. // Remove the temporary script at the end of this function. If it fails, it's not a big deal
  71. // since it will be over-written next time and won't affect the Zip file, so we can ignore
  72. // any failure.
  73. defer { try? FileManager.default.removeItem(at: scriptPath) }
  74. // Let the process call directly into the temporary shell script we created.
  75. let task = Process()
  76. task.arguments = [scriptPath.path]
  77. if #available(OSX 10.13, *) {
  78. if let workingDir = workingDir {
  79. task.currentDirectoryURL = workingDir
  80. }
  81. // Explicitly use `/bin/bash`. Investigate whether or not we can use `/usr/local/env`
  82. task.executableURL = URL(fileURLWithPath: "/bin/bash")
  83. } else {
  84. // Assign the old `currentDirectoryPath` property if `currentDirectoryURL` isn't available.
  85. if let workingDir = workingDir {
  86. task.currentDirectoryPath = workingDir.path
  87. }
  88. task.launchPath = "/bin/bash"
  89. }
  90. // Assign a pipe to read as soon as data is available, log it to the console if requested, but
  91. // also keep an array of the output in memory so we can pass it back to functions.
  92. // Assign a pipe to grab the output, and handle it differently if we want to stream the results
  93. // to the console or not.
  94. let pipe = Pipe()
  95. task.standardOutput = pipe
  96. let outHandle = pipe.fileHandleForReading
  97. var output: [String] = []
  98. // If we want to output to the console, create a readabilityHandler and save each line along the
  99. // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
  100. // commands (such as any xcodebuild) can run much, much faster.
  101. if outputToConsole {
  102. outHandle.readabilityHandler = { pipe in
  103. // This will be run any time data is sent to the pipe. We want to print it and store it for
  104. // later. Ignore any non-valid Strings.
  105. guard let line = String(data: pipe.availableData, encoding: .utf8) else {
  106. print("Could not get data from pipe for command \(command): \(pipe.availableData)")
  107. return
  108. }
  109. if line != "" {
  110. output.append(line)
  111. }
  112. print(line)
  113. }
  114. // Also set the termination handler on the task in order to stop the readabilityHandler from
  115. // parsing any more data from the task.
  116. task.terminationHandler = { t in
  117. guard let stdOut = t.standardOutput as? Pipe else { return }
  118. stdOut.fileHandleForReading.readabilityHandler = nil
  119. }
  120. }
  121. // Launch the task and wait for it to exit. This will trigger the above readabilityHandler
  122. // method and will redirect the command output back to the console for quick feedback.
  123. if outputToConsole {
  124. print("Running command: \(command).")
  125. print("----------------- COMMAND OUTPUT -----------------")
  126. }
  127. task.launch()
  128. // If we are not outputting to the console, there is a possibility that
  129. // the output pipe gets filled (e.g. when running a command that generates
  130. // lots of output). In this scenario, the process will hang and
  131. // `task.waitUntilExit()` will never return. To work around this issue,
  132. // calling `outHandle.readDataToEndOfFile()` before `task.waitUntilExit()`
  133. // will read from the pipe until the process ends.
  134. var outData: Data!
  135. if !outputToConsole {
  136. outData = outHandle.readDataToEndOfFile()
  137. }
  138. task.waitUntilExit()
  139. if outputToConsole { print("----------------- END COMMAND OUTPUT -----------------") }
  140. let fullOutput: String
  141. if outputToConsole {
  142. fullOutput = output.joined(separator: "\n")
  143. } else {
  144. // Force unwrapping since we know it's UTF8 coming from the console.
  145. fullOutput = String(data: outData, encoding: .utf8)!
  146. }
  147. // Check if the task succeeded or not, and return the failure code if it didn't.
  148. guard task.terminationStatus == 0 else {
  149. return Result.error(code: task.terminationStatus, output: fullOutput)
  150. }
  151. // The command was successful, return the output.
  152. return Result.success(output: fullOutput)
  153. }
  154. /// Execute a command in the user's shell. This creates a temporary shell script and runs the
  155. /// command from there, instead of calling via `Process()` directly in order to include the
  156. /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt
  157. /// other commands.
  158. ///
  159. /// This is a variation of `executeCommandFromScript` that also does error handling internally.
  160. ///
  161. /// - Parameters:
  162. /// - command: The command to run in the shell.
  163. /// - outputToConsole: A flag if the command output should be written to the console as well.
  164. /// - workingDir: An optional working directory to run the shell command in.
  165. /// - Returns: A Result containing output information from the command.
  166. static func executeCommand(_ command: String,
  167. outputToConsole: Bool = true,
  168. workingDir: URL? = nil) {
  169. if logOnly {
  170. print(command)
  171. return
  172. }
  173. let result = Shell.executeCommandFromScript(command, workingDir: workingDir)
  174. switch result {
  175. case let .error(code, output):
  176. fatalError("""
  177. `\(command)` failed with exit code \(code) while trying to install pods:
  178. Output from `\(command)`:
  179. \(output)
  180. """)
  181. case let .success(output):
  182. // Print the output to the console and return the information for all installed pods.
  183. print(output)
  184. }
  185. }
  186. }