ShellUtils.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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. // The user's .bash_profile, if it exists, is sourced to modify the
  59. // shell's PATH with any configuration (e.g. adding Ruby to path)
  60. // that may be needed to run the given command.
  61. let contents = """
  62. export PATH="/usr/local/bin:/usr/local/git/current/bin:$PATH"
  63. export LANG="en_US.UTF-8"
  64. BASH_PROFILE_PATH="~/.bash_profile"
  65. if [ -f "$BASH_PROFILE_PATH" ]; then
  66. source $BASH_PROFILE_PATH
  67. fi
  68. \(command)
  69. """
  70. try contents.write(to: scriptPath, atomically: true, encoding: .utf8)
  71. } catch let FileManager.FileError.failedToCreateDirectory(path, error) {
  72. fatalError("Could not execute shell command: \(command) - could not create temporary " +
  73. "script directory at \(path). \(error)")
  74. } catch {
  75. fatalError("Could not execute shell command: \(command) - unexpected error. \(error)")
  76. }
  77. // Remove the temporary script at the end of this function. If it fails, it's not a big deal
  78. // since it will be over-written next time and won't affect the Zip file, so we can ignore
  79. // any failure.
  80. defer { try? FileManager.default.removeItem(at: scriptPath) }
  81. // Let the process call directly into the temporary shell script we created.
  82. let task = Process()
  83. task.arguments = [scriptPath.path]
  84. if #available(OSX 10.13, *) {
  85. if let workingDir = workingDir {
  86. task.currentDirectoryURL = workingDir
  87. }
  88. // Explicitly use `/bin/bash`. Investigate whether or not we can use `/usr/local/env`
  89. task.executableURL = URL(fileURLWithPath: "/bin/bash")
  90. } else {
  91. // Assign the old `currentDirectoryPath` property if `currentDirectoryURL` isn't available.
  92. if let workingDir = workingDir {
  93. task.currentDirectoryPath = workingDir.path
  94. }
  95. task.launchPath = "/bin/bash"
  96. }
  97. // Assign a pipe to read as soon as data is available, log it to the console if requested, but
  98. // also keep an array of the output in memory so we can pass it back to functions.
  99. // Assign a pipe to grab the output, and handle it differently if we want to stream the results
  100. // to the console or not.
  101. let pipe = Pipe()
  102. task.standardOutput = pipe
  103. let outHandle = pipe.fileHandleForReading
  104. var output: [String] = []
  105. // If we want to output to the console, create a readabilityHandler and save each line along the
  106. // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some
  107. // commands (such as any xcodebuild) can run much, much faster.
  108. if outputToConsole {
  109. outHandle.readabilityHandler = { pipe in
  110. // This will be run any time data is sent to the pipe. We want to print it and store it for
  111. // later. Ignore any non-valid Strings.
  112. guard let line = String(data: pipe.availableData, encoding: .utf8) else {
  113. print("Could not get data from pipe for command \(command): \(pipe.availableData)")
  114. return
  115. }
  116. if line != "" {
  117. output.append(line)
  118. }
  119. print(line)
  120. }
  121. // Also set the termination handler on the task in order to stop the readabilityHandler from
  122. // parsing any more data from the task.
  123. task.terminationHandler = { t in
  124. guard let stdOut = t.standardOutput as? Pipe else { return }
  125. stdOut.fileHandleForReading.readabilityHandler = nil
  126. }
  127. }
  128. // Launch the task and wait for it to exit. This will trigger the above readabilityHandler
  129. // method and will redirect the command output back to the console for quick feedback.
  130. if outputToConsole {
  131. print("Running command: \(command).")
  132. print("----------------- COMMAND OUTPUT -----------------")
  133. }
  134. task.launch()
  135. // If we are not outputting to the console, there is a possibility that
  136. // the output pipe gets filled (e.g. when running a command that generates
  137. // lots of output). In this scenario, the process will hang and
  138. // `task.waitUntilExit()` will never return. To work around this issue,
  139. // calling `outHandle.readDataToEndOfFile()` before `task.waitUntilExit()`
  140. // will read from the pipe until the process ends.
  141. var outData: Data!
  142. if !outputToConsole {
  143. outData = outHandle.readDataToEndOfFile()
  144. }
  145. task.waitUntilExit()
  146. if outputToConsole { print("----------------- END COMMAND OUTPUT -----------------") }
  147. let fullOutput: String
  148. if outputToConsole {
  149. fullOutput = output.joined(separator: "\n")
  150. } else {
  151. // Force unwrapping since we know it's UTF8 coming from the console.
  152. fullOutput = String(data: outData, encoding: .utf8)!
  153. }
  154. // Check if the task succeeded or not, and return the failure code if it didn't.
  155. guard task.terminationStatus == 0 else {
  156. return Result.error(code: task.terminationStatus, output: fullOutput)
  157. }
  158. // The command was successful, return the output.
  159. return Result.success(output: fullOutput)
  160. }
  161. /// Execute a command in the user's shell. This creates a temporary shell script and runs the
  162. /// command from there, instead of calling via `Process()` directly in order to include the
  163. /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt
  164. /// other commands.
  165. ///
  166. /// This is a variation of `executeCommandFromScript` that also does error handling internally.
  167. ///
  168. /// - Parameters:
  169. /// - command: The command to run in the shell.
  170. /// - outputToConsole: A flag if the command output should be written to the console as well.
  171. /// - workingDir: An optional working directory to run the shell command in.
  172. /// - Returns: A Result containing output information from the command.
  173. static func executeCommand(_ command: String,
  174. outputToConsole: Bool = true,
  175. workingDir: URL? = nil) {
  176. if logOnly {
  177. print(command)
  178. return
  179. }
  180. let result = Shell.executeCommandFromScript(command, workingDir: workingDir)
  181. switch result {
  182. case let .error(code, output):
  183. fatalError("""
  184. `\(command)` failed with exit code \(code) while trying to install pods:
  185. Output from `\(command)`:
  186. \(output)
  187. """)
  188. case let .success(output):
  189. // Print the output to the console and return the information for all installed pods.
  190. print(output)
  191. }
  192. }
  193. }