FileManager+Utils.swift 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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. // Extensions to FileManager that make scripting easier or cleaner for error reporting.
  18. public extension FileManager {
  19. // MARK: - Helper Enum Declarations
  20. /// Describes a type of file to be searched for.
  21. enum SearchFileType {
  22. /// All files, not including folders.
  23. case allFiles
  24. /// All folders with a `.bundle` extension.
  25. case bundles
  26. /// A directory with an optional name. If name is `nil`, all directories will be matched.
  27. case directories(name: String?)
  28. /// All folders with a `.framework` extension.
  29. case frameworks
  30. /// All headers with a `.h` extension.
  31. case headers
  32. /// All files with the `.storyboard` extension.
  33. case storyboards
  34. }
  35. // MARK: - Error Declarations
  36. /// Errors that can be used to propagate up through the script related to files.
  37. enum FileError: Error {
  38. case directoryNotFound(path: String)
  39. case failedToCreateDirectory(path: String, error: Error)
  40. case writeToFileFailed(file: String, error: Error)
  41. }
  42. /// Errors that can occur during a recursive search operation.
  43. enum RecursiveSearchError: Error {
  44. case failedToCreateEnumerator(forDirectory: URL)
  45. }
  46. // MARK: - Directory Management
  47. /// Convenience function to determine if there's a directory at the given file URL using existing
  48. /// FileManager calls.
  49. func directoryExists(at url: URL) -> Bool {
  50. var isDir: ObjCBool = false
  51. let exists = fileExists(atPath: url.path, isDirectory: &isDir)
  52. return exists && isDir.boolValue
  53. }
  54. /// Convenience function to determine if a given file URL is a directory.
  55. func isDirectory(at url: URL) -> Bool {
  56. return directoryExists(at: url)
  57. }
  58. /// Returns the URL to the source Pod cache directory, and creates it if it doesn't exist.
  59. func sourcePodCacheDirectory(withSubdir subdir: String = "") throws -> URL {
  60. let cacheDir = FileManager.default.temporaryDirectory(withName: "cache")
  61. let cacheRoot = cacheDir.appendingPathComponents([subdir])
  62. if directoryExists(at: cacheRoot) {
  63. return cacheRoot
  64. }
  65. // The cache root folder doesn't exist yet, create it.
  66. try createDirectory(at: cacheRoot, withIntermediateDirectories: true)
  67. return cacheRoot
  68. }
  69. /// Removes a directory or file if it exists. This is helpful to clean up error handling for
  70. /// checks that
  71. /// shouldn't fail. The only situation this could potentially fail is permission errors or if a
  72. /// folder is open in Finder, and in either state the user needs to close the window or fix the
  73. /// permissions. A fatal error will be thrown in those situations.
  74. func removeIfExists(at url: URL) {
  75. guard directoryExists(at: url) || fileExists(atPath: url.path) else { return }
  76. do {
  77. try removeItem(at: url)
  78. } catch {
  79. fatalError("""
  80. Tried to remove directory \(url) but it failed - close any Finder windows and try again.
  81. Error: \(error)
  82. """)
  83. }
  84. }
  85. /// Enable a single unique temporary workspace per execution with a sortable and readable
  86. /// timestamp.
  87. private static func timeStamp() -> String {
  88. let formatter = DateFormatter()
  89. formatter.dateFormat = "yyyy-MM-dd'T'HH-mm-ss"
  90. return formatter.string(from: Date())
  91. }
  92. static let unique: String = timeStamp()
  93. /// Allow clients to override default location for temporary directory creation
  94. static var buildRoot: URL?
  95. static func registerBuildRoot(buildRoot: URL) {
  96. FileManager.buildRoot = buildRoot
  97. }
  98. /// Returns a deterministic path of a temporary directory for the given name. Note: This does
  99. /// *not* create the directory if it doesn't exist, merely generates the name for creation.
  100. func temporaryDirectory(withName name: String) -> URL {
  101. // Get access to the temporary directory. This could be passed in via `LaunchArgs`, or use the
  102. // default temporary directory.
  103. let tempDir: URL
  104. if let root = FileManager.buildRoot {
  105. tempDir = root
  106. } else
  107. if #available(OSX 10.12, *) {
  108. tempDir = temporaryDirectory
  109. } else {
  110. tempDir = URL(fileURLWithPath: NSTemporaryDirectory())
  111. }
  112. // Organize all temporary directories into a "ZipRelease" directory.
  113. let unique = FileManager.unique
  114. let zipDir = tempDir.appendingPathComponent("ZipRelease/" + unique, isDirectory: true)
  115. return zipDir.appendingPathComponent(name, isDirectory: true)
  116. }
  117. // MARK: Searching
  118. /// Recursively search for a set of items in a particular directory.
  119. func recursivelySearch(for type: SearchFileType, in dir: URL) throws -> [URL] {
  120. // Throw an error so an appropriate error can be logged from the caller.
  121. guard directoryExists(at: dir) else {
  122. throw FileError.directoryNotFound(path: dir.path)
  123. }
  124. // We have a directory, create an enumerator to do a recursive search.
  125. let keys: [URLResourceKey] = [.nameKey, .isDirectoryKey]
  126. guard let dirEnumerator = enumerator(at: dir, includingPropertiesForKeys: keys) else {
  127. // Throw an error so an appropriate error can be logged from the caller.
  128. throw RecursiveSearchError.failedToCreateEnumerator(forDirectory: dir)
  129. }
  130. // Recursively search using the enumerator, adding any matches to the array.
  131. var matches: [URL] = []
  132. var foundXcframework = false // Ignore .frameworks after finding an xcframework.
  133. while let fileURL = dirEnumerator.nextObject() as? URL {
  134. switch type {
  135. case .allFiles:
  136. // Skip directories, include everything else.
  137. guard !isDirectory(at: fileURL) else { continue }
  138. matches.append(fileURL)
  139. case let .directories(name):
  140. // Skip any non-directories.
  141. guard directoryExists(at: fileURL) else { continue }
  142. // Get the name of the directory we're searching for. If there's not a specific name
  143. // being searched for, add it as a match and move on.
  144. guard let name = name else {
  145. matches.append(fileURL)
  146. continue
  147. }
  148. // If the last path component is a match, it's a directory we're looking for!
  149. if fileURL.lastPathComponent == name {
  150. matches.append(fileURL)
  151. }
  152. case .bundles:
  153. // The only thing of interest is the path extension being ".bundle".
  154. if fileURL.pathExtension == "bundle" {
  155. matches.append(fileURL)
  156. }
  157. case .headers:
  158. if fileURL.pathExtension == "h" {
  159. matches.append(fileURL)
  160. }
  161. case .storyboards:
  162. // The only thing of interest is the path extension being ".storyboard".
  163. if fileURL.pathExtension == "storyboard" {
  164. matches.append(fileURL)
  165. }
  166. case .frameworks:
  167. // We care if it's a directory and has a .xcframework or .framework extension.
  168. if directoryExists(at: fileURL) {
  169. if fileURL.pathExtension == "xcframework" {
  170. matches.append(fileURL)
  171. foundXcframework = true
  172. } else if !foundXcframework, fileURL.pathExtension == "framework" {
  173. matches.append(fileURL)
  174. }
  175. }
  176. }
  177. }
  178. return matches
  179. }
  180. }