ResourcesManager.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. import Utils
  18. /// Functions related to managing resources. Intentionally empty, this enum is used as a namespace.
  19. enum ResourcesManager {}
  20. extension ResourcesManager {
  21. /// Recursively searches a directory for any sign of resources: `.bundle` folders, or a non-empty
  22. /// directory called "Resources".
  23. ///
  24. /// - Parameter dir: The directory to search for any sign of resources.
  25. /// - Returns: True if any resources could be found, otherwise false.
  26. /// - Throws: A FileManager API that was thrown while searching.
  27. static func directoryContainsResources(_ dir: URL) throws -> Bool {
  28. // First search for any .bundle files.
  29. let fileManager = FileManager.default
  30. let bundles = try fileManager.recursivelySearch(for: .bundles, in: dir)
  31. // Stop searching if there were any bundles found.
  32. if !bundles.isEmpty { return true }
  33. // Next, search for any non-empty Resources directories.
  34. let existingResources = try fileManager.recursivelySearch(for: .directories(name: "Resources"),
  35. in: dir)
  36. for resource in existingResources {
  37. let fileList = try fileManager.contentsOfDirectory(atPath: resource.path)
  38. if !fileList.isEmpty { return true }
  39. }
  40. // At this point: no bundles were found, and either there were no Resources directories or they
  41. // were all empty. Safe to say this directory doesn't contain any resources.
  42. return false
  43. }
  44. /// Packages all resources in a directory (recursively) - compiles them, puts them in a
  45. /// bundle, embeds them in the adjacent .framework file, and cleans up any empty Resources
  46. /// directories.
  47. ///
  48. /// - Parameters:
  49. /// - fromDir: The directory to search for resources.
  50. /// - toDir: The Resources directory to dump all resource bundles in.
  51. /// - bundlesToRemove: Any bundles to remove (name of the bundle, not a full path).
  52. /// - Returns: True if any resources were moved and packaged, otherwise false.
  53. /// - Throws: Any file system errors that occur.
  54. @discardableResult
  55. static func packageAllResources(containedIn dir: URL,
  56. bundlesToIgnore: [String] = []) throws -> Bool {
  57. let resourcesFound = try directoryContainsResources(dir)
  58. // Quit early if there are no resources to deal with.
  59. if !resourcesFound { return false }
  60. let fileManager = FileManager.default
  61. // There are three possibilities for resources at this point:
  62. // 1. A `.bundle` could be packaged in a `Resources` directory inside of a framework.
  63. // - We want to keep these where they are.
  64. // 2. A `.bundle` could be packaged in a `Resources` directory outside of a framework.
  65. // - We want to move these into the framework adjacent to the `Resources` dir.
  66. // 3. A `Resources` directory that still needs to be compiled, outside of a framework.
  67. // - These need to be compiled into `.bundles` and moved into the relevant framework
  68. // directory.
  69. let allResourceDirs = try fileManager.recursivelySearch(for: .directories(name: "Resources"),
  70. in: dir)
  71. for resourceDir in allResourceDirs {
  72. // Situation 1: Ignore any Resources directories that are already in the .framework.
  73. let parentDir = resourceDir.deletingLastPathComponent()
  74. guard parentDir.pathExtension != "framework" else {
  75. print("Found a Resources directory inside \(parentDir), no action necessary.")
  76. continue
  77. }
  78. // Store the paths to bundles that are found or newly assembled.
  79. var bundles: [URL] = []
  80. // Situation 2: Find any bundles that already exist but aren't included in the framework.
  81. bundles += try fileManager.recursivelySearch(for: .bundles, in: resourceDir)
  82. // Situation 3: Create any leftover bundles in this directory.
  83. bundles += try createBundles(fromDir: resourceDir)
  84. // Filter out any explicitly ignored bundles.
  85. bundles.removeAll(where: { bundlesToIgnore.contains($0.lastPathComponent) })
  86. // Find the right framework for these bundles to be embedded in - the folder structure is
  87. // likely:
  88. // - ProductFoo
  89. // - Frameworks
  90. // - ProductFoo.framework
  91. // - Resources
  92. // - BundleFoo.bundle
  93. // - BundleBar.bundle
  94. // - etc.
  95. // If there are more than one frameworks in the "Frameworks" directory, we can try to match
  96. // the name of the bundle and the framework but if it doesn't match, fail because we don't
  97. // know what bundle the resources belong to. This isn't the case now for any Firebase products
  98. // but it's a good flag to raise in case that happens in the future.
  99. let frameworksDir = parentDir.appendingPathComponent("Frameworks")
  100. guard fileManager.directoryExists(at: frameworksDir) else {
  101. fatalError("Could not package resources in \(resourceDir): Frameworks directory doesn't " +
  102. "exist: \(frameworksDir)")
  103. }
  104. let contents = try fileManager.contentsOfDirectory(atPath: frameworksDir.path)
  105. switch contents.count {
  106. case 0:
  107. // No Frameworks exist.
  108. fatalError("Could not find framework file to package Resources in \(resourceDir). " +
  109. "\(frameworksDir) is empty.")
  110. case 1:
  111. // Force unwrap is fine here since we know the first one exists.
  112. let frameworkName = contents.first!
  113. let frameworkResources = frameworksDir.appendingPathComponents([frameworkName, "Resources"])
  114. // Move all the bundles into the Resources directory for that framework. This will create
  115. // the directory if it doesn't exist.
  116. try moveAllFiles(bundles, toDir: frameworkResources)
  117. default:
  118. // More than one framework is found. Try a last ditch effort of lining up the name, and if
  119. // that doesn't work fail out.
  120. for bundle in bundles {
  121. // Get the name of the bundle without any suffix.
  122. let name = bundle.lastPathComponent.replacingOccurrences(of: ".bundle", with: "")
  123. guard contents.contains(name) else {
  124. fatalError("Attempting to embed \(name).bundle into a framework but there are too " +
  125. "many frameworks to choose from in \(frameworksDir).")
  126. }
  127. // We somehow have a match, embed that bundle in the framework and try the next one!
  128. let frameworkResources = frameworksDir.appendingPathComponents([name, "Resources"])
  129. try moveAllFiles([bundle], toDir: frameworkResources)
  130. }
  131. }
  132. }
  133. // Let the caller know we've modified resources.
  134. return true
  135. }
  136. /// Recursively searches for bundles in `dir` and moves them to the Resources directory
  137. /// `resourceDir`.
  138. ///
  139. /// - Parameters:
  140. /// - dir: The directory to search for Resource bundles.
  141. /// - resourceDir: The destination Resources directory. This function will create the Resources
  142. /// directory if it doesn't exist.
  143. /// - keepOriginal: Do a copy instead of a move.
  144. /// - Returns: An array of URLs pointing to the newly located bundles.
  145. /// - Throws: Any file system errors that occur.
  146. @discardableResult
  147. static func moveAllBundles(inDirectory dir: URL,
  148. to resourceDir: URL,
  149. keepOriginal: Bool = false) throws -> [URL] {
  150. let fileManager = FileManager.default
  151. let allBundles = try fileManager.recursivelySearch(for: .bundles, in: dir)
  152. // If no bundles are found, return an empty array since nothing was done (but there wasn't an
  153. // error).
  154. guard !allBundles.isEmpty else { return [] }
  155. // Move the found bundles into the Resources directory.
  156. let bundlesMoved = try moveAllFiles(allBundles, toDir: resourceDir, keepOriginal: keepOriginal)
  157. // Remove any empty Resources directories left over as part of the move.
  158. removeEmptyResourcesDirectories(in: dir)
  159. return bundlesMoved
  160. }
  161. /// Searches for and attempts to remove all empty "Resources" directories in a given directory.
  162. /// This is a recrusive search.
  163. ///
  164. /// - Parameter dir: The directory to recursively search for Resources directories in.
  165. static func removeEmptyResourcesDirectories(in dir: URL) {
  166. // Find all the Resources directories to begin with.
  167. let fileManager = FileManager.default
  168. guard let resourceDirs = try? fileManager
  169. .recursivelySearch(for: .directories(name: "Resources"),
  170. in: dir) else {
  171. print("Attempted to remove empty resource directories, but it failed. This shouldn't be " +
  172. "classified as an error, but something to look out for.")
  173. return
  174. }
  175. // Get the contents of each directory and if it's empty, remove it.
  176. for resourceDir in resourceDirs {
  177. guard let contents = try? fileManager.contentsOfDirectory(atPath: resourceDir.path) else {
  178. print("WARNING: Failed to get contents of apparent Resources directory at \(resourceDir)")
  179. continue
  180. }
  181. // Remove the directory if it's empty. Only warn if it's not successful, since it's not a
  182. // requirement but a nice to have.
  183. if contents.isEmpty {
  184. do {
  185. try fileManager.removeItem(at: resourceDir)
  186. } catch {
  187. print("WARNING: Failed to remove empty Resources directory while cleaning up folder " +
  188. "heirarchy: \(error)")
  189. }
  190. }
  191. }
  192. }
  193. // MARK: Private Helpers
  194. /// Creates bundles for all folders in the directory passed in, and will compile
  195. ///
  196. /// - Parameter dir: A directory containing folders to make into bundles.
  197. /// - Returns: An array of filepaths to bundles that were packaged.
  198. /// - Throws: Any file manager errors thrown.
  199. private static func createBundles(fromDir dir: URL) throws -> [URL] {
  200. // Get all the folders in the "Resources" directory and loop through them.
  201. let fileManager = FileManager.default
  202. var bundles: [URL] = []
  203. let contents = try fileManager.contentsOfDirectory(atPath: dir.path)
  204. for fileOrFolder in contents {
  205. let fullPath = dir.appendingPathComponent(fileOrFolder)
  206. // The dir itself may contain resource files at its root. If so, we may need to package these
  207. // in the future but print a warning for now.
  208. guard fileManager.isDirectory(at: fullPath) else {
  209. print("WARNING: Found a file in the Resources directory, this may need to be packaged: " +
  210. "\(fullPath)")
  211. continue
  212. }
  213. if fullPath.lastPathComponent.hasSuffix("bundle") {
  214. // It's already a bundle, so no need to create one.
  215. continue
  216. }
  217. // It's a folder. Generate the name and location based on the folder name.
  218. let name = fullPath.lastPathComponent + ".bundle"
  219. let location = dir.appendingPathComponent(name)
  220. // Copy the existing Resources folder to the new bundle location.
  221. try fileManager.copyItem(at: fullPath, to: location)
  222. // Compile any storyboards that exist in the new bundle.
  223. compileStoryboards(inDir: location)
  224. bundles.append(location)
  225. }
  226. return bundles
  227. }
  228. /// Finds and compiles all `.storyboard` files in a directory, removing the original file.
  229. private static func compileStoryboards(inDir dir: URL) {
  230. let fileManager = FileManager.default
  231. let storyboards: [URL]
  232. do {
  233. storyboards = try fileManager.recursivelySearch(for: .storyboards, in: dir)
  234. } catch {
  235. fatalError("Failed to search for storyboards in directory: \(error)")
  236. }
  237. // Compile each storyboard, then remove it.
  238. for storyboard in storyboards {
  239. // Compiled storyboards have the extension `storyboardc`.
  240. let compiledPath = storyboard.deletingPathExtension().appendingPathExtension("storyboardc")
  241. // Run the command and throw an error if it fails.
  242. let command = "ibtool --compile \(compiledPath.path) \(storyboard.path)"
  243. let result = Shell.executeCommandFromScript(command)
  244. switch result {
  245. case .success:
  246. // Remove the original storyboard file and continue.
  247. do {
  248. try fileManager.removeItem(at: storyboard)
  249. } catch {
  250. fatalError("Could not remove storyboard file \(storyboard) from bundle after " +
  251. "compilation: \(error)")
  252. }
  253. case let .error(code, output):
  254. fatalError("Failed to compile storyboard \(storyboard): error \(code) \(output)")
  255. }
  256. }
  257. }
  258. /// Moves all files passed in to the destination dir, keeping the same filename.
  259. ///
  260. /// - Parameters:
  261. /// - files: URLs to files to move.
  262. /// - destinationDir: Destination directory to move all the files. Creates the directory if it
  263. /// doesn't exist.
  264. /// - keepOriginal: Do a copy instead of a move.
  265. /// - Throws: Any file system errors that occur.
  266. @discardableResult
  267. private static func moveAllFiles(_ files: [URL], toDir destinationDir: URL,
  268. keepOriginal: Bool = false) throws -> [URL] {
  269. let fileManager = FileManager.default
  270. if !fileManager.directoryExists(at: destinationDir) {
  271. try fileManager.createDirectory(at: destinationDir, withIntermediateDirectories: true)
  272. }
  273. var filesMoved: [URL] = []
  274. for file in files {
  275. // Create the destination URL by using the filename of the file but prefix of the
  276. // destinationDir.
  277. let destination = destinationDir.appendingPathComponent(file.lastPathComponent)
  278. if keepOriginal {
  279. try fileManager.copyItem(at: file, to: destination)
  280. } else {
  281. try fileManager.moveItem(at: file, to: destination)
  282. }
  283. filesMoved.append(destination)
  284. }
  285. return filesMoved
  286. }
  287. }