| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- /*
- * 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
- import Utils
- /// Functions related to managing resources. Intentionally empty, this enum is used as a namespace.
- enum ResourcesManager {}
- extension ResourcesManager {
- /// Recursively searches a directory for any sign of resources: `.bundle` folders, or a non-empty
- /// directory called "Resources".
- ///
- /// - Parameter dir: The directory to search for any sign of resources.
- /// - Returns: True if any resources could be found, otherwise false.
- /// - Throws: A FileManager API that was thrown while searching.
- static func directoryContainsResources(_ dir: URL) throws -> Bool {
- // First search for any .bundle files.
- let fileManager = FileManager.default
- let bundles = try fileManager.recursivelySearch(for: .bundles, in: dir)
- // Stop searching if there were any bundles found.
- if !bundles.isEmpty { return true }
- // Next, search for any non-empty Resources directories.
- let existingResources = try fileManager.recursivelySearch(for: .directories(name: "Resources"),
- in: dir)
- for resource in existingResources {
- let fileList = try fileManager.contentsOfDirectory(atPath: resource.path)
- if !fileList.isEmpty { return true }
- }
- // At this point: no bundles were found, and either there were no Resources directories or they
- // were all empty. Safe to say this directory doesn't contain any resources.
- return false
- }
- /// Packages all resources in a directory (recursively) - compiles them, puts them in a
- /// bundle, embeds them in the adjacent .framework file, and cleans up any empty Resources
- /// directories.
- ///
- /// - Parameters:
- /// - fromDir: The directory to search for resources.
- /// - toDir: The Resources directory to dump all resource bundles in.
- /// - bundlesToRemove: Any bundles to remove (name of the bundle, not a full path).
- /// - Returns: True if any resources were moved and packaged, otherwise false.
- /// - Throws: Any file system errors that occur.
- @discardableResult
- static func packageAllResources(containedIn dir: URL,
- bundlesToIgnore: [String] = []) throws -> Bool {
- let resourcesFound = try directoryContainsResources(dir)
- // Quit early if there are no resources to deal with.
- if !resourcesFound { return false }
- let fileManager = FileManager.default
- // There are three possibilities for resources at this point:
- // 1. A `.bundle` could be packaged in a `Resources` directory inside of a framework.
- // - We want to keep these where they are.
- // 2. A `.bundle` could be packaged in a `Resources` directory outside of a framework.
- // - We want to move these into the framework adjacent to the `Resources` dir.
- // 3. A `Resources` directory that still needs to be compiled, outside of a framework.
- // - These need to be compiled into `.bundles` and moved into the relevant framework
- // directory.
- let allResourceDirs = try fileManager.recursivelySearch(for: .directories(name: "Resources"),
- in: dir)
- for resourceDir in allResourceDirs {
- // Situation 1: Ignore any Resources directories that are already in the .framework.
- let parentDir = resourceDir.deletingLastPathComponent()
- guard parentDir.pathExtension != "framework" else {
- print("Found a Resources directory inside \(parentDir), no action necessary.")
- continue
- }
- // Store the paths to bundles that are found or newly assembled.
- var bundles: [URL] = []
- // Situation 2: Find any bundles that already exist but aren't included in the framework.
- bundles += try fileManager.recursivelySearch(for: .bundles, in: resourceDir)
- // Situation 3: Create any leftover bundles in this directory.
- bundles += try createBundles(fromDir: resourceDir)
- // Filter out any explicitly ignored bundles.
- bundles.removeAll(where: { bundlesToIgnore.contains($0.lastPathComponent) })
- // Find the right framework for these bundles to be embedded in - the folder structure is
- // likely:
- // - ProductFoo
- // - Frameworks
- // - ProductFoo.framework
- // - Resources
- // - BundleFoo.bundle
- // - BundleBar.bundle
- // - etc.
- // If there are more than one frameworks in the "Frameworks" directory, we can try to match
- // the name of the bundle and the framework but if it doesn't match, fail because we don't
- // know what bundle the resources belong to. This isn't the case now for any Firebase products
- // but it's a good flag to raise in case that happens in the future.
- let frameworksDir = parentDir.appendingPathComponent("Frameworks")
- guard fileManager.directoryExists(at: frameworksDir) else {
- fatalError("Could not package resources in \(resourceDir): Frameworks directory doesn't " +
- "exist: \(frameworksDir)")
- }
- let contents = try fileManager.contentsOfDirectory(atPath: frameworksDir.path)
- switch contents.count {
- case 0:
- // No Frameworks exist.
- fatalError("Could not find framework file to package Resources in \(resourceDir). " +
- "\(frameworksDir) is empty.")
- case 1:
- // Force unwrap is fine here since we know the first one exists.
- let frameworkName = contents.first!
- let frameworkResources = frameworksDir.appendingPathComponents([frameworkName, "Resources"])
- // Move all the bundles into the Resources directory for that framework. This will create
- // the directory if it doesn't exist.
- try moveAllFiles(bundles, toDir: frameworkResources)
- default:
- // More than one framework is found. Try a last ditch effort of lining up the name, and if
- // that doesn't work fail out.
- for bundle in bundles {
- // Get the name of the bundle without any suffix.
- let name = bundle.lastPathComponent.replacingOccurrences(of: ".bundle", with: "")
- guard contents.contains(name) else {
- fatalError("Attempting to embed \(name).bundle into a framework but there are too " +
- "many frameworks to choose from in \(frameworksDir).")
- }
- // We somehow have a match, embed that bundle in the framework and try the next one!
- let frameworkResources = frameworksDir.appendingPathComponents([name, "Resources"])
- try moveAllFiles([bundle], toDir: frameworkResources)
- }
- }
- }
- // Let the caller know we've modified resources.
- return true
- }
- /// Recursively searches for bundles in `dir` and moves them to the Resources directory
- /// `resourceDir`.
- ///
- /// - Parameters:
- /// - dir: The directory to search for Resource bundles.
- /// - resourceDir: The destination Resources directory. This function will create the Resources
- /// directory if it doesn't exist.
- /// - keepOriginal: Do a copy instead of a move.
- /// - Returns: An array of URLs pointing to the newly located bundles.
- /// - Throws: Any file system errors that occur.
- @discardableResult
- static func moveAllBundles(inDirectory dir: URL,
- to resourceDir: URL,
- keepOriginal: Bool = false) throws -> [URL] {
- let fileManager = FileManager.default
- let allBundles = try fileManager.recursivelySearch(for: .bundles, in: dir)
- // If no bundles are found, return an empty array since nothing was done (but there wasn't an
- // error).
- guard !allBundles.isEmpty else { return [] }
- // Move the found bundles into the Resources directory.
- let bundlesMoved = try moveAllFiles(allBundles, toDir: resourceDir, keepOriginal: keepOriginal)
- // Remove any empty Resources directories left over as part of the move.
- removeEmptyResourcesDirectories(in: dir)
- return bundlesMoved
- }
- /// Searches for and attempts to remove all empty "Resources" directories in a given directory.
- /// This is a recrusive search.
- ///
- /// - Parameter dir: The directory to recursively search for Resources directories in.
- static func removeEmptyResourcesDirectories(in dir: URL) {
- // Find all the Resources directories to begin with.
- let fileManager = FileManager.default
- guard let resourceDirs = try? fileManager
- .recursivelySearch(for: .directories(name: "Resources"),
- in: dir) else {
- print("Attempted to remove empty resource directories, but it failed. This shouldn't be " +
- "classified as an error, but something to look out for.")
- return
- }
- // Get the contents of each directory and if it's empty, remove it.
- for resourceDir in resourceDirs {
- guard let contents = try? fileManager.contentsOfDirectory(atPath: resourceDir.path) else {
- print("WARNING: Failed to get contents of apparent Resources directory at \(resourceDir)")
- continue
- }
- // Remove the directory if it's empty. Only warn if it's not successful, since it's not a
- // requirement but a nice to have.
- if contents.isEmpty {
- do {
- try fileManager.removeItem(at: resourceDir)
- } catch {
- print("WARNING: Failed to remove empty Resources directory while cleaning up folder " +
- "heirarchy: \(error)")
- }
- }
- }
- }
- // MARK: Private Helpers
- /// Creates bundles for all folders in the directory passed in, and will compile
- ///
- /// - Parameter dir: A directory containing folders to make into bundles.
- /// - Returns: An array of filepaths to bundles that were packaged.
- /// - Throws: Any file manager errors thrown.
- private static func createBundles(fromDir dir: URL) throws -> [URL] {
- // Get all the folders in the "Resources" directory and loop through them.
- let fileManager = FileManager.default
- var bundles: [URL] = []
- let contents = try fileManager.contentsOfDirectory(atPath: dir.path)
- for fileOrFolder in contents {
- let fullPath = dir.appendingPathComponent(fileOrFolder)
- // The dir itself may contain resource files at its root. If so, we may need to package these
- // in the future but print a warning for now.
- guard fileManager.isDirectory(at: fullPath) else {
- print("WARNING: Found a file in the Resources directory, this may need to be packaged: " +
- "\(fullPath)")
- continue
- }
- if fullPath.lastPathComponent.hasSuffix("bundle") {
- // It's already a bundle, so no need to create one.
- continue
- }
- // It's a folder. Generate the name and location based on the folder name.
- let name = fullPath.lastPathComponent + ".bundle"
- let location = dir.appendingPathComponent(name)
- // Copy the existing Resources folder to the new bundle location.
- try fileManager.copyItem(at: fullPath, to: location)
- // Compile any storyboards that exist in the new bundle.
- compileStoryboards(inDir: location)
- bundles.append(location)
- }
- return bundles
- }
- /// Finds and compiles all `.storyboard` files in a directory, removing the original file.
- private static func compileStoryboards(inDir dir: URL) {
- let fileManager = FileManager.default
- let storyboards: [URL]
- do {
- storyboards = try fileManager.recursivelySearch(for: .storyboards, in: dir)
- } catch {
- fatalError("Failed to search for storyboards in directory: \(error)")
- }
- // Compile each storyboard, then remove it.
- for storyboard in storyboards {
- // Compiled storyboards have the extension `storyboardc`.
- let compiledPath = storyboard.deletingPathExtension().appendingPathExtension("storyboardc")
- // Run the command and throw an error if it fails.
- let command = "ibtool --compile \(compiledPath.path) \(storyboard.path)"
- let result = Shell.executeCommandFromScript(command)
- switch result {
- case .success:
- // Remove the original storyboard file and continue.
- do {
- try fileManager.removeItem(at: storyboard)
- } catch {
- fatalError("Could not remove storyboard file \(storyboard) from bundle after " +
- "compilation: \(error)")
- }
- case let .error(code, output):
- fatalError("Failed to compile storyboard \(storyboard): error \(code) \(output)")
- }
- }
- }
- /// Moves all files passed in to the destination dir, keeping the same filename.
- ///
- /// - Parameters:
- /// - files: URLs to files to move.
- /// - destinationDir: Destination directory to move all the files. Creates the directory if it
- /// doesn't exist.
- /// - keepOriginal: Do a copy instead of a move.
- /// - Throws: Any file system errors that occur.
- @discardableResult
- private static func moveAllFiles(_ files: [URL], toDir destinationDir: URL,
- keepOriginal: Bool = false) throws -> [URL] {
- let fileManager = FileManager.default
- if !fileManager.directoryExists(at: destinationDir) {
- try fileManager.createDirectory(at: destinationDir, withIntermediateDirectories: true)
- }
- var filesMoved: [URL] = []
- for file in files {
- // Create the destination URL by using the filename of the file but prefix of the
- // destinationDir.
- let destination = destinationDir.appendingPathComponent(file.lastPathComponent)
- if keepOriginal {
- try fileManager.copyItem(at: file, to: destination)
- } else {
- try fileManager.moveItem(at: file, to: destination)
- }
- filesMoved.append(destination)
- }
- return filesMoved
- }
- }
|