|
| 1 | +import ArgumentParser |
| 2 | +import Foundation |
| 3 | +import Rainbow |
| 4 | +import FengNiaoKit |
| 5 | +import PathKit |
| 6 | + |
| 7 | +@main |
| 8 | +struct FengNiaoCommand: ParsableCommand { |
| 9 | + private static let appVersion = "0.11.0" |
| 10 | + private static let exitUnusedResources: Int32 = 1 |
| 11 | + private static let exitUsage: Int32 = 64 |
| 12 | + |
| 13 | + static let configuration = CommandConfiguration( |
| 14 | + commandName: "fengniao", |
| 15 | + abstract: "Find and delete unused resources in Xcode projects.", |
| 16 | + version: appVersion |
| 17 | + ) |
| 18 | + |
| 19 | + @Option( |
| 20 | + name: .shortAndLong, |
| 21 | + help: "Root path of your Xcode project. Default is current folder." |
| 22 | + ) |
| 23 | + var project: String = "." |
| 24 | + |
| 25 | + @Flag( |
| 26 | + name: .long, |
| 27 | + help: "Delete the found unused files without asking." |
| 28 | + ) |
| 29 | + var force: Bool = false |
| 30 | + |
| 31 | + @Option( |
| 32 | + name: [.short, .long], |
| 33 | + parsing: .upToNextOption, |
| 34 | + help: "Exclude paths from search." |
| 35 | + ) |
| 36 | + var exclude: [String] = [] |
| 37 | + |
| 38 | + @Option( |
| 39 | + name: [.short, .long], |
| 40 | + parsing: .upToNextOption, |
| 41 | + help: "Resource file extensions need to be searched. Default is 'imageset jpg png gif pdf'" |
| 42 | + ) |
| 43 | + var resourceExtensions: [String] = ["imageset", "jpg", "png", "gif", "pdf"] |
| 44 | + |
| 45 | + @Option( |
| 46 | + name: [.short, .long], |
| 47 | + parsing: .upToNextOption, |
| 48 | + help: "In which types of files we should search for resource usage. Default is 'm mm swift xib storyboard plist'" |
| 49 | + ) |
| 50 | + var fileExtensions: [String] = ["h", "m", "mm", "swift", "xib", "storyboard", "plist"] |
| 51 | + |
| 52 | + @Flag( |
| 53 | + name: .long, |
| 54 | + help: "Skip the Project file (.pbxproj) reference cleaning. By skipping it, the project file will be left untouched. You may want to skip ths step if you are trying to build multiple projects with dependency and keep .pbxproj unchanged while compiling." |
| 55 | + ) |
| 56 | + var skipProjReference: Bool = false |
| 57 | + |
| 58 | + @Flag( |
| 59 | + name: .long, |
| 60 | + help: "Print results as xcode warnings and return non zero code if any." |
| 61 | + ) |
| 62 | + var xcodeWarnings: Bool = false |
| 63 | + |
| 64 | + @Flag( |
| 65 | + name: .long, |
| 66 | + help: "List unused files and exit without prompting." |
| 67 | + ) |
| 68 | + var listOnly: Bool = false |
| 69 | + |
| 70 | + func run() throws { |
| 71 | + let fengNiao = FengNiao( |
| 72 | + projectPath: project, |
| 73 | + excludedPaths: exclude, |
| 74 | + resourceExtensions: resourceExtensions, |
| 75 | + searchInFileExtensions: fileExtensions |
| 76 | + ) |
| 77 | + |
| 78 | + let unusedFiles: [FileInfo] |
| 79 | + do { |
| 80 | + print("Searching unused file. This may take a while...") |
| 81 | + unusedFiles = try fengNiao.unusedFiles() |
| 82 | + } catch { |
| 83 | + guard let e = error as? FengNiaoError else { |
| 84 | + print("Unknown Error: \(error)".red.bold) |
| 85 | + throw ExitCode(Self.exitUsage) |
| 86 | + } |
| 87 | + switch e { |
| 88 | + case .noResourceExtension: |
| 89 | + print("You need to specify some resource extensions as search target. Use --resource-extensions to specify.".red.bold) |
| 90 | + case .noFileExtension: |
| 91 | + print("You need to specify some file extensions to search in. Use --file-extensions to specify.".red.bold) |
| 92 | + } |
| 93 | + throw ExitCode(Self.exitUsage) |
| 94 | + } |
| 95 | + |
| 96 | + if unusedFiles.isEmpty { |
| 97 | + print("😎 Hu, you have no unused resources in path: \(Path(project).absolute()).".green.bold) |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + if xcodeWarnings { |
| 102 | + for file in unusedFiles.sorted(by: { $0.size > $1.size }) { |
| 103 | + print("\(file.path.string): warning: Unused resource of size \(file.readableSize)") |
| 104 | + } |
| 105 | + throw ExitCode(Self.exitUnusedResources) |
| 106 | + } |
| 107 | + |
| 108 | + if listOnly { |
| 109 | + let size = unusedFiles.reduce(0) { $0 + $1.size }.fn_readableSize |
| 110 | + for file in unusedFiles.sorted(by: { $0.size > $1.size }) { |
| 111 | + print("\(file.readableSize) \(file.path.string)") |
| 112 | + } |
| 113 | + print("\(unusedFiles.count) unused files are found. Total Size: \(size)".yellow.bold) |
| 114 | + return |
| 115 | + } |
| 116 | + |
| 117 | + if !force { |
| 118 | + var result = promptResult(files: unusedFiles) |
| 119 | + while result == .list { |
| 120 | + for file in unusedFiles.sorted(by: { $0.size > $1.size }) { |
| 121 | + print("\(file.readableSize) \(file.path.string)") |
| 122 | + } |
| 123 | + result = promptResult(files: unusedFiles) |
| 124 | + } |
| 125 | + |
| 126 | + switch result { |
| 127 | + case .list: |
| 128 | + fatalError() |
| 129 | + case .delete: |
| 130 | + break |
| 131 | + case .ignore: |
| 132 | + print("Ignored. Nothing to do, bye!".green.bold) |
| 133 | + return |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + print("Deleting unused files...⚙".bold) |
| 138 | + |
| 139 | + let (deleted, failed) = FengNiao.delete(unusedFiles) |
| 140 | + guard failed.isEmpty else { |
| 141 | + print("\(unusedFiles.count - failed.count) unused files are deleted. But we encountered some error while deleting these \(failed.count) files:".yellow.bold) |
| 142 | + for (fileInfo, err) in failed { |
| 143 | + print("\(fileInfo.path.string) - \(err.localizedDescription)") |
| 144 | + } |
| 145 | + throw ExitCode(Self.exitUsage) |
| 146 | + } |
| 147 | + |
| 148 | + print("\(unusedFiles.count) unused files are deleted.".green.bold) |
| 149 | + |
| 150 | + if !skipProjReference { |
| 151 | + if let children = try? Path(project).absolute().children() { |
| 152 | + print("Now Deleting unused Reference in project.pbxproj...⚙".bold) |
| 153 | + for path in children where path.lastComponent.hasSuffix("xcodeproj") { |
| 154 | + let pbxproj = path + "project.pbxproj" |
| 155 | + FengNiao.deleteReference(projectFilePath: pbxproj, deletedFiles: deleted) |
| 156 | + } |
| 157 | + print("Unused Reference deleted successfully.".green.bold) |
| 158 | + } |
| 159 | + } |
| 160 | + } |
| 161 | +} |
0 commit comments