Files
firebase-ios-sdk/ReleaseTooling/Sources/ZipBuilder/ResourcesManager.swift
T

325 lines
14 KiB
Swift

/*
* 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
}
// 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
}
}