├── .gitignore ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Resources ├── usage.gif └── work-around.gif └── Sources └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /build 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ken Tominaga 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=/usr/local 2 | BUILD_TOOL=xcodebuild 3 | 4 | EXECUTABLE_NAME=carthage-input-files 5 | PROJECT_NAME=CarthageInputFiles 6 | XCODEFLAGS=-project $(PROJECT_NAME).xcodeproj 7 | 8 | CARTHAGEINPUTFILES_EXECUTABLE=./.build/release/$(PROJECT_NAME) 9 | 10 | SWIFT_COMMAND=/usr/bin/swift 11 | SWIFT_BUILD_COMMAND=$(SWIFT_COMMAND) build 12 | SWIFT_TEST_COMMAND=$(SWIFT_COMMAND) test 13 | 14 | debug: 15 | $(SWIFT_BUILD_COMMAND) 16 | 17 | release: 18 | $(SWIFT_BUILD_COMMAND) --configuration release 19 | 20 | update: 21 | $(SWIFT_COMMAND) package update 22 | 23 | generate: 24 | $(SWIFT_COMMAND) package generate-xcodeproj 25 | 26 | test: 27 | $(SWIFT_TEST_COMMAND) 28 | 29 | install: 30 | $(SWIFT_BUILD_COMMAND) --configuration release 31 | mkdir -p $(PREFIX)/bin 32 | cp -f $(CARTHAGEINPUTFILES_EXECUTABLE) $(PREFIX)/bin/$(EXECUTABLE_NAME) 33 | 34 | uninstall: 35 | rm -f $(PREFIX)/bin/$(EXECUTABLE_NAME) 36 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "CarthageInputFiles", 5 | dependencies: [ 6 | .Package(url: "git@github.com:kylef/Commander.git", majorVersion: 0, minor: 6), 7 | .Package(url: "https://github.com/onevcat/Rainbow", majorVersion: 2), 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CarthageInputFiles 2 | 3 | This command lets you free from setting framework paths every time when using `Carthage`. 4 | 5 | You don't have to write manually `$(SRCROOT)/Carthage/Build/iOS/*.framework` any more :) 6 | 7 | ![](./Resources/usage.gif) 8 | 9 | ## Usage 10 | 11 | Add `Run Script` for `Carthage` in Xcode. 12 | 13 | `/usr/local/bin/carthage copy-frameworks` 14 | 15 | ```swift 16 | carthage update 17 | carthage-input-files YourXcodeProject.xcodeproj 18 | ``` 19 | 20 | ## Installation 21 | 22 | - Clone this repository 23 | 24 | `git clone https://github.com/ken0nek/CarthageInputFiles.git` 25 | 26 | or 27 | 28 | `git clone git@github.com:ken0nek/CarthageInputFiles.git` 29 | 30 | - Make 31 | 32 | `make install` 33 | 34 | `carthage-input-files` command will be moved to `/usr/local/bin` by default 35 | 36 | ## Little tricks 37 | 38 | After executing `carthage-input-files` command, you will see huge diff in `project.pbxproj` because of format. 39 | This problem will be fixed by editing some settings. 40 | 41 | For example, click `+` and then click `-` :P 42 | 43 | ![](./Resources/work-around.gif) 44 | 45 | ## Future features 46 | 47 | - [x] Write only new frameworks 48 | - [x] Select frameworks you want to exclude 49 | - [x] Specify target 50 | - [ ] Fix format problem (openStep <-> xml) 51 | - [ ] Add Run Script for Carthage 52 | 53 | -------------------------------------------------------------------------------- /Resources/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ken0nek/CarthageInputFiles/a4de3f0c1c925701041998f1f52a2817defcc414/Resources/usage.gif -------------------------------------------------------------------------------- /Resources/work-around.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ken0nek/CarthageInputFiles/a4de3f0c1c925701041998f1f52a2817defcc414/Resources/work-around.gif -------------------------------------------------------------------------------- /Sources/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Commander 3 | import Rainbow 4 | 5 | private let cartfile = "Cartfile" 6 | private let pbxproj = "project.pbxproj" 7 | private let platforms = ["ios": "iOS", 8 | "mac": "Mac", 9 | "watchos" : "watchOS", 10 | "tvos": "tvOS"] 11 | 12 | private extension String { 13 | 14 | func appendingPath(_ aString: String) -> String { 15 | 16 | if hasSuffix("/") { 17 | return self.appending(aString) 18 | } else { 19 | return self.appending("/\(aString)") 20 | } 21 | } 22 | 23 | } 24 | 25 | command( 26 | Argument("project", description: "Xcode project to process (*.xcodeproj)"), 27 | Option("platform", "ios", description: "platform (ios, mac, watchos, tvos)"), 28 | Option("prefix", "$(SRCROOT)/Carthage/Build/") 29 | ) { project, platform, prefix in 30 | 31 | guard (project.hasSuffix("/") && project.hasSuffix("xcodeproj/")) || project.hasSuffix("xcodeproj") else { 32 | print("Please input valid xcode project (*.xcodeproj)".red) 33 | return 34 | } 35 | 36 | guard let p = platforms[platform] else { 37 | print("Please input valid platform name (ios, mac, watchos, tvos)".red) 38 | return 39 | } 40 | 41 | print("Processing \(project)...\n") 42 | 43 | let dir = FileManager.default.currentDirectoryPath 44 | let cartfilePath = dir.appendingPath(cartfile) 45 | let cartfileURL = URL(fileURLWithPath: cartfilePath) 46 | 47 | do { 48 | if try cartfileURL.checkResourceIsReachable() { 49 | print("Found \(cartfilePath)\n") 50 | } 51 | } catch let error { 52 | print("Error: \(error.localizedDescription)".red) 53 | print("Create `Cartfile` and execute `carthage update`") 54 | return 55 | } 56 | 57 | let pbxprojPath = dir.appendingPath(project).appendingPath(pbxproj) 58 | let pbxprojURL = URL(fileURLWithPath: pbxprojPath) 59 | 60 | do { 61 | if try pbxprojURL.checkResourceIsReachable() { 62 | print("Found \(pbxprojPath)\n") 63 | } 64 | } catch let error { 65 | print("Error: \(error.localizedDescription)".red) 66 | print("Cannot process \(pbxprojPath)") 67 | return 68 | } 69 | 70 | guard let data = try? Data(contentsOf: pbxprojURL) else { return } 71 | 72 | guard let dic = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else { return } 73 | var dicToWrite = dic 74 | 75 | guard let objects = dic["objects"] as? [String: Any] else { return } 76 | var objectsToWrite = objects 77 | 78 | var targets: [(key: String, name: String, phaseKeys: [String])] = [] 79 | var carthageShellScripts: [(key: String, inputPaths: [String])] = [] 80 | 81 | for (k, v) in objects { 82 | guard let dd = v as? [String: Any] else { continue } 83 | 84 | guard let isa = dd["isa"] as? String else { continue } 85 | 86 | switch isa { 87 | case "PBXNativeTarget": 88 | guard let targetName = dd["name"] as? String else { continue } 89 | guard let buildPhases = dd["buildPhases"] as? [String] else { continue } 90 | 91 | targets.append((k, targetName, buildPhases)) 92 | case "PBXShellScriptBuildPhase": 93 | guard let shellScript = dd["shellScript"] as? String else { continue } 94 | guard shellScript.hasSuffix("copy-frameworks") else { continue } 95 | guard let inputPaths = dd["inputPaths"] as? [String] else { continue } 96 | 97 | carthageShellScripts.append((k, inputPaths)) 98 | default: 99 | continue 100 | } 101 | } 102 | 103 | var selectedIndex = -1 104 | while true { 105 | print("Which target do you want to process?".blue) 106 | targets.map { $0.name }.enumerated().forEach { 107 | print("\($0): \($1)") 108 | } 109 | guard let str = readLine(), let index = Int(str.trimmingCharacters(in: .whitespacesAndNewlines)) else { continue } 110 | 111 | guard 0 <= index && index <= targets.count - 1 else { 112 | print("Error: Please input valide number".red) 113 | continue 114 | } 115 | 116 | selectedIndex = index 117 | break 118 | } 119 | 120 | let selectedTarget = targets[selectedIndex] 121 | 122 | var key = "" 123 | var currentInputPaths: [String] = [] 124 | for script in carthageShellScripts { 125 | if selectedTarget.phaseKeys.contains(script.key) { 126 | key = script.key 127 | currentInputPaths = script.inputPaths 128 | break 129 | } 130 | } 131 | 132 | guard !key.isEmpty else { 133 | print("This target has no run scripts for Carthage".red) 134 | print("[Xcode] -> [Targets] -> [Build Phases]: [+ New Run Script Phase]") 135 | print("and add this command `/usr/local/bin/carthage copy-frameworks`") 136 | return 137 | } 138 | 139 | let prefixWithPlatform = prefix.appendingPath(p) 140 | let buildPathWithPlatform = "Carthage/Build".appendingPath(p) 141 | 142 | print("Searching frameworks in \(buildPathWithPlatform)...\n") 143 | 144 | guard let enumerator = FileManager.default.enumerator(atPath: buildPathWithPlatform) else { return } 145 | 146 | var frameworks: [String] = [] 147 | for f in enumerator { 148 | guard let file = f as? String else { return } 149 | 150 | // Only include top level framework 151 | guard file.hasSuffix(".framework") && file.components(separatedBy: "/").count == 1 else { continue } 152 | frameworks.append(file) 153 | } 154 | 155 | guard !frameworks.isEmpty else { 156 | print("No frameworks found :(".red) 157 | print("execute `carthage update` or edit `Cartfile`") 158 | return 159 | } 160 | 161 | let frameworksWithPrefix = frameworks.map { prefixWithPlatform.appendingPath($0) } 162 | 163 | let new = Set(frameworksWithPrefix) 164 | let current = Set(currentInputPaths) 165 | let diff = new.subtracting(current) 166 | 167 | guard !diff.isEmpty else { 168 | print("Nothing to do.\n") 169 | print("Current Input Files:\n") 170 | currentInputPaths.forEach { print($0) } 171 | return 172 | } 173 | 174 | var frameworksDiff = [String](diff) 175 | 176 | // TODO: Error handling 177 | while true { 178 | 179 | print("\nThese frameworks will be newly added to input files for Carthage Run Scripts\n") 180 | frameworksDiff.enumerated().forEach { print("\($0): \($1)") } 181 | 182 | print("\nThe result will be:\n") 183 | let tmp = currentInputPaths + frameworksDiff 184 | tmp.forEach { print($0.green) } 185 | 186 | print("\nis it OK? [[y]/n]") 187 | print("Please type the number if there are any frameworks you would like to exclude. (Example: 1, 3)".blue) 188 | guard let yesno = readLine() else { return } 189 | 190 | if yesno == "y" || yesno.isEmpty { 191 | break 192 | } else if yesno == "n" { 193 | return 194 | } else { 195 | 196 | let exclude = yesno.characters.split(separator: ",").flatMap { Int(String($0).trimmingCharacters(in: .whitespacesAndNewlines)) } 197 | let excludeUnique = [Int](Set(exclude)) 198 | 199 | guard excludeUnique.count <= frameworksDiff.count else { 200 | print("Invalid input: Too many frameworks to exclude".red) 201 | return 202 | } 203 | 204 | guard let max = excludeUnique.max(), max <= frameworksDiff.count - 1 else { 205 | print("Invalid input: Out of bounds".red) 206 | return 207 | } 208 | 209 | excludeUnique.forEach { frameworksDiff.remove(at: $0) } 210 | 211 | guard !frameworksDiff.isEmpty else { 212 | print("Nothing to do.\n") 213 | print("Current Input Files:\n") 214 | currentInputPaths.forEach { print($0) } 215 | return 216 | } 217 | } 218 | } 219 | 220 | let frameworksToWrite = currentInputPaths + frameworksDiff 221 | 222 | var target = objects[key] as! [String: Any] 223 | target["inputPaths"] = frameworksToWrite 224 | objectsToWrite[key] = target 225 | dicToWrite["objects"] = objectsToWrite 226 | 227 | // .openStep: Property list format kCFPropertyListOpenStepFormat not supported for writing 228 | guard let dataToWrite = try? PropertyListSerialization.data(fromPropertyList: dicToWrite, format: .xml, options: .allZeros) else { return } 229 | 230 | do { 231 | try dataToWrite.write(to: pbxprojURL, options: .atomic) 232 | } catch let error { 233 | print("Error: \(error.localizedDescription)") 234 | return 235 | } 236 | 237 | print("Finished!") 238 | print("Please check if everything is fine :)") 239 | 240 | }.run() 241 | --------------------------------------------------------------------------------