Files
firebase-ios-sdk/ReleaseTooling/Sources/Utils/ShellUtils.swift
T

198 lines
8.3 KiB
Swift
Raw Normal View History

/*
* 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.
2020-10-15 07:48:42 -07:00
public enum Shell {}
2020-10-15 07:48:42 -07:00
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)
}
2020-10-15 07:48:42 -07:00
/// 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"
source ~/.bash_profile
\(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
}
2020-10-15 07:48:42 -07:00
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()
task.waitUntilExit()
if outputToConsole { print("----------------- END COMMAND OUTPUT -----------------") }
let fullOutput: String
if outputToConsole {
fullOutput = output.joined(separator: "\n")
} else {
let outData = outHandle.readDataToEndOfFile()
// 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)
}
2020-10-15 07:48:42 -07:00
/// 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 {
2020-10-15 07:48:42 -07:00
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):
2020-10-15 07:48:42 -07:00
// Print the output to the console and return the information for all installed pods.
print(output)
}
}
}