ShellUtils.swift 8.3 KB

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