diff --git a/CHANGELOG.md b/CHANGELOG.md index aef3b3c..c33328c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## master +* Introduce `--include-existing` (`--no-include-existing`) option + [Toshihiro Suzuki](https://github.com/toshi0383) + [#8](https://github.com/toshi0383/xcconfig-extractor/pull/8) + ## 0.2.0 * Rename `trim-duplicates` to `--no-trim-duplicates` diff --git a/README.md b/README.md index 71cf9f5..d5efbaa 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,28 @@ Make sure you setup each configurations correctly on Xcode. Options: --no-trim-duplicates [default: false] - Don't extract duplicated lines to common xcconfig files, simply map each buildSettings to one file. --no-edit-pbxproj [default: false] - Do not modify pbxproj. + --include-existing [default: true] - `#include` already configured xcconfigs. ``` +# Build Setting Validation +⚠️***Waring***⚠️ + +You should check app's Build Settings hasn't been affected by applying this tool. +xcconfig does not allow any `$(inherited)` from `#include`ing xcconfigs. (See: https://github.com/toshi0383/xcconfig-extractor/pull/8#issuecomment-298234943) So if you have any existing xcconfig configured on your project, it might cause problems. + +Recommended way to check Build Settings is to run command like below. Make sure outputs does not change between before and after. +```bash +$ xcodebuild -showBuildSettings -configuration Release > before +$ # apply xcconfig-extractor +$ xcodebuild -showBuildSettings -configuration Release > after +$ diff before after # should prints nothing! +``` + +If output changed, you should manually fix it. (e.g. by adding missing variable to target's(top level) xcconfig.) +[This article](https://pewpewthespells.com/blog/xcconfig_guide.html#BuildSettingInheritance) is helpful to understand how inheritance works. + # TODOs -- Add Tests +- Add More Tests # License MIT diff --git a/Sources/PBXProj/BuildConfigurationList.swift b/Sources/PBXProj/BuildConfigurationList.swift index 57d01f3..39121b6 100644 --- a/Sources/PBXProj/BuildConfigurationList.swift +++ b/Sources/PBXProj/BuildConfigurationList.swift @@ -9,29 +9,39 @@ import Foundation public struct BuildConfigurationList: IsaObject { - public let object: [String: Any] + public let key: String + public let rawObject: [String: Any] public let buildConfigurations: [BuildConfiguration] public let defaultConfigurationName: String? public let defaultConfigurationIsVisible: String - public init(_ o: [String: Any], objects: [String: Any]) { - self.object = o + public init?(key: String, value o: [String: Any], objects: [String: Any]) { + guard IsaType(object: o) == .XCConfigurationList else { + return nil + } + self.key = key + self.rawObject = o self.defaultConfigurationName = o["defaultConfigurationName"] as? String let buildConfigurationKeys = o["buildConfigurations"] as! [String] - self.buildConfigurations = buildConfigurationKeys.map { key in objects[key] as! [String: Any] } - .map(BuildConfiguration.init) + self.buildConfigurations = buildConfigurationKeys.map { key in (key, objects) } + .flatMap(BuildConfiguration.init) self.defaultConfigurationIsVisible = o["defaultConfigurationIsVisible"] as! String } } public struct BuildConfiguration: IsaObject { - public let object: [String: Any] + public let key: String + public let rawObject: [String: Any] public let name: String - public let baseConfigurationReference: String? + public let baseConfigurationReference: FileReference? public let buildSettings: [String: Any] - public init(object o: [String: Any]) { - self.object = o + public init?(key: String, value o: [String: Any], objects: [String: Any]) { + guard IsaType(object: o) == .XCBuildConfiguration else { + return nil + } + self.key = key + self.rawObject = o self.name = o["name"] as! String - self.baseConfigurationReference = o["baseConfigurationReference"] as? String + self.baseConfigurationReference = FileReference(key: o["baseConfigurationReference"], objects: objects) self.buildSettings = o["buildSettings"] as! [String: Any] } } diff --git a/Sources/PBXProj/FileReference.swift b/Sources/PBXProj/FileReference.swift new file mode 100644 index 0000000..f2c2b20 --- /dev/null +++ b/Sources/PBXProj/FileReference.swift @@ -0,0 +1,37 @@ +// +// FileReference.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +public enum FileType: String { + case xcconfig = "text.xcconfig" +} + +public struct FileReference: IsaObject { + public let key: String + public let rawObject: [String : Any] + public let lastKnownFileType: FileType + public let path: String + public let sourceTree: String + + // custom property + public let fullPath: String + + public init?(key: String, value o: [String : Any], objects: [String : Any]) { + guard IsaType(object: o) == .PBXFileReference else { + return nil + } + self.key = key + self.rawObject = o + self.lastKnownFileType = FileType(rawValue: o["lastKnownFileType"] as! String)! + self.path = o["path"] as! String + let fullPath = findPaths(to: key, objects: objects) + [self.path] + self.fullPath = fullPath.joined(separator: "/") + self.sourceTree = o["sourceTree"] as! String + } +} diff --git a/Sources/PBXProj/Functions.swift b/Sources/PBXProj/Functions.swift new file mode 100644 index 0000000..ab910ec --- /dev/null +++ b/Sources/PBXProj/Functions.swift @@ -0,0 +1,24 @@ +// +// Functions.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +func findPaths(to id: String, objects: [String: Any]) -> [String] { + for (k, v) in objects { + if let o = v as? [String: Any], let group = Group(key: k, value: o, objects: objects) { + if group.children.contains(id) { + if let path = group.path { + return findPaths(to: k, objects: objects) + [path] + } else { + return findPaths(to: k, objects: objects) + } + } + } + } + return [] +} diff --git a/Sources/PBXProj/Group.swift b/Sources/PBXProj/Group.swift new file mode 100644 index 0000000..9a2137b --- /dev/null +++ b/Sources/PBXProj/Group.swift @@ -0,0 +1,27 @@ +// +// Group.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +public struct Group: IsaObject { + public let rawObject: [String : Any] + public let key: String + public let children: [String] + public let path: String? + public let name: String? + public init?(key: String, value o: [String : Any], objects: [String : Any]) { + guard IsaType(object: o) == .PBXGroup else { + return nil + } + self.key = key + self.rawObject = o + self.children = o["children"] as! [String] + self.path = o["path"] as? String + self.name = o["name"] as? String + } +} diff --git a/Sources/PBXProj/IsaObject.swift b/Sources/PBXProj/IsaObject.swift index aacd759..b2dea1f 100644 --- a/Sources/PBXProj/IsaObject.swift +++ b/Sources/PBXProj/IsaObject.swift @@ -19,15 +19,37 @@ public enum IsaType: String { case PBXGroup case PBXFrameworksBuildPhase case PBXBuildRule + case PBXBuildFile + case PBXFileReference + case PBXContainerItemProxy + case PBXVariantGroup + case PBXTargetDependency + case PBXCopyFilesBuildPhase + case XCVersionGroup + init(object: [String: Any]) { + self.init(rawValue: object["isa"] as! String)! + } } public protocol IsaObject { + var key: String { get } var isa: IsaType { get } - var object: [String: Any] { get } + var rawObject: [String: Any] { get } + init?(key: String, value: [String: Any], objects: [String: Any]) + init?(key: Any?, objects: [String: Any]) } extension IsaObject { public var isa: IsaType { - return IsaType(rawValue: object["isa"] as! String)! + return IsaType(rawValue: rawObject["isa"] as! String)! + } + public init?(key: Any?, objects: [String: Any]) { + guard let key = key as? String else { + return nil + } + guard let o = objects[key] as? [String: Any] else { + return nil + } + self.init(key: key, value: o, objects: objects) } } diff --git a/Sources/PBXProj/NativeTarget.swift b/Sources/PBXProj/NativeTarget.swift index c849387..bf61839 100644 --- a/Sources/PBXProj/NativeTarget.swift +++ b/Sources/PBXProj/NativeTarget.swift @@ -17,7 +17,8 @@ public enum ProductType: String { } public struct NativeTarget: IsaObject { - public let object: [String: Any] + public let key: String + public let rawObject: [String: Any] public let name: String public let productName: String @@ -27,8 +28,12 @@ public struct NativeTarget: IsaObject { public let dependencies: [Any] // TODO public let buildPhases: [String] // TODO public let buildConfigurationList: BuildConfigurationList - init(target o: [String: Any], objects: [String: Any]) { - self.object = o + public init?(key: String, value o: [String: Any], objects: [String: Any]) { + guard IsaType(object: o) == .PBXNativeTarget else { + return nil + } + self.key = key + self.rawObject = o self.name = o["name"] as! String self.productName = o["productName"] as! String self.productType = ProductType(rawValue: o["productType"] as! String)! @@ -37,6 +42,6 @@ public struct NativeTarget: IsaObject { self.dependencies = o["dependencies"] as! [Any] self.buildPhases = o["buildPhases"] as! [String] let buildConfigurationListKey = o["buildConfigurationList"] as! String - self.buildConfigurationList = BuildConfigurationList(objects[buildConfigurationListKey] as! [String: Any], objects: objects) + self.buildConfigurationList = BuildConfigurationList(key: buildConfigurationListKey, objects: objects)! } } diff --git a/Sources/PBXProj/Pbxproj.swift b/Sources/PBXProj/Pbxproj.swift index a73a5da..ce2a004 100644 --- a/Sources/PBXProj/Pbxproj.swift +++ b/Sources/PBXProj/Pbxproj.swift @@ -37,7 +37,7 @@ public struct Pbxproj { fatalError("rootObject or objects not found!") } self.objects = objects - let rootObject = Project(objects[rootObjectKey] as! [String: Any], objects: objects) + let rootObject = Project(key: rootObjectKey, objects: objects)! self.rootObject = rootObject } func object(for key: String) -> T { diff --git a/Sources/PBXProj/Project.swift b/Sources/PBXProj/Project.swift index 7754b54..15fc223 100644 --- a/Sources/PBXProj/Project.swift +++ b/Sources/PBXProj/Project.swift @@ -9,7 +9,8 @@ import Foundation public struct Project: IsaObject { - public let object: [String: Any] + public let key: String + public let rawObject: [String: Any] public let attributes: [String: Any] public let buildConfigurationList: BuildConfigurationList public let compatibilityVersion: String @@ -21,11 +22,15 @@ public struct Project: IsaObject { public let projectDirPath: String public let projectRoot: String? public let targets: [NativeTarget] - public init(_ o: [String: Any], objects: [String: Any]) { - self.object = o + public init?(key: String, value o: [String: Any], objects: [String: Any]) { + guard IsaType(object: o) == .PBXProject else { + return nil + } + self.key = key + self.rawObject = o self.attributes = o["attributes"] as! [String: Any] let buildConfigurationListKey = o["buildConfigurationList"] as! String - self.buildConfigurationList = BuildConfigurationList(objects[buildConfigurationListKey] as! [String: Any], objects: objects) + self.buildConfigurationList = BuildConfigurationList(key: buildConfigurationListKey, objects: objects)! self.compatibilityVersion = o["compatibilityVersion"] as! String self.developmentRegion = o["developmentRegion"] as! String self.hasScannedForEncodings = o["hasScannedForEncodings"] as! String @@ -35,8 +40,7 @@ public struct Project: IsaObject { self.projectDirPath = o["projectDirPath"] as! String self.projectRoot = o["projectRoot"] as? String self.targets = (o["targets"] as! [String]).map { k in - let target = objects[k] as! [String: Any] - return NativeTarget(target: target, objects: objects) + return NativeTarget(key: k, objects: objects)! } } } diff --git a/Sources/xcconfig-extractor/Config.swift b/Sources/xcconfig-extractor/Config.swift new file mode 100644 index 0000000..a9efea8 --- /dev/null +++ b/Sources/xcconfig-extractor/Config.swift @@ -0,0 +1,17 @@ +// +// Config.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +struct Config { + static let version = "0.2.0" + let isIncludeExisting: Bool + init(isIncludeExisting: Bool) { + self.isIncludeExisting = isIncludeExisting + } +} diff --git a/Sources/xcconfig-extractor/ErrorReporter.swift b/Sources/xcconfig-extractor/ErrorReporter.swift new file mode 100644 index 0000000..77e03d4 --- /dev/null +++ b/Sources/xcconfig-extractor/ErrorReporter.swift @@ -0,0 +1,30 @@ +// +// ErrorReporter.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +func printStdError(_ message: String) { + fputs("\(ANSI.red)\(message)\(ANSI.reset)", stderr) +} + +private enum ANSI : String, CustomStringConvertible { + case red = "\u{001B}[0;31m" + case green = "\u{001B}[0;32m" + case yellow = "\u{001B}[0;33m" + + case bold = "\u{001B}[0;1m" + case reset = "\u{001B}[0;0m" + + var description:String { + if isatty(STDOUT_FILENO) > 0 { + return rawValue + } + return "" + } +} + diff --git a/Sources/xcconfig-extractor/ResultFormatter.swift b/Sources/xcconfig-extractor/ResultFormatter.swift new file mode 100644 index 0000000..8835491 --- /dev/null +++ b/Sources/xcconfig-extractor/ResultFormatter.swift @@ -0,0 +1,30 @@ +// +// ResultFormatter.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation + +class ResultFormatter { + let config: Config + init(config: Config) { + self.config = config + } + private var header: [String] { + let signature = "// Generated using xcconfig-extractor \(Config.version) by Toshihiro Suzuki - https://github.com/toshi0383/xcconfig-extractor" + return [signature] + } + private func addInclude(filePath: String) -> String { + return "#include \"\(filePath)\"" + } + func format(result: ResultObject, includes: [String] = []) -> [String] { + return header + + result.includes.map(addInclude) + + includes.map(addInclude) + + result.settings + + ["\n"] + } +} diff --git a/Sources/xcconfig-extractor/ResultObject.swift b/Sources/xcconfig-extractor/ResultObject.swift new file mode 100644 index 0000000..f9a412e --- /dev/null +++ b/Sources/xcconfig-extractor/ResultObject.swift @@ -0,0 +1,33 @@ +// +// ResultObject.swift +// xcconfig-extractor +// +// Created by Toshihiro Suzuki on 2017/04/30. +// Copyright © 2017 Toshihiro Suzuki. All rights reserved. +// + +import Foundation +import PathKit + +class ResultObject: Equatable { + let path: Path + var settings: [String] + let targetName: String? + let configurationName: String? + var includes: [String] + init(path: Path, settings: [String], targetName: String? = nil, configurationName: String? = nil, includes: [String] = []) { + self.path = path + self.settings = settings + self.targetName = targetName + self.configurationName = configurationName + self.includes = includes + } +} +func ==(lhs: ResultObject, rhs: ResultObject) -> Bool { + guard lhs.path == rhs.path else { return false } + guard lhs.settings == rhs.settings else { return false } + guard lhs.targetName == rhs.targetName else { return false } + guard lhs.configurationName == rhs.configurationName else { return false } + guard lhs.includes == rhs.includes else { return false } + return true +} diff --git a/Sources/xcconfig-extractor/main.swift b/Sources/xcconfig-extractor/main.swift index e6e078d..9014161 100644 --- a/Sources/xcconfig-extractor/main.swift +++ b/Sources/xcconfig-extractor/main.swift @@ -12,54 +12,50 @@ import PathKit import PBXProj import Utilities -let version = "0.2.0" -let header = ["// Generated using xcconfig-extractor \(version) by Toshihiro Suzuki - https://github.com/toshi0383/xcconfig-extractor"] - -func write(to path: Path, settings: [String], includes: [String] = []) throws { - let formatted = format(settings, with: includes) - let data = (formatted.joined(separator: "\n") as NSString).data(using: String.Encoding.utf8.rawValue)! +func write(to path: Path, lines: [String] = []) throws { + let data = (lines.joined(separator: "\n") as NSString).data(using: String.Encoding.utf8.rawValue)! try path.write(data) } -func format(_ result: [String], with includes: [String] = []) -> [String] { - return header + includes.map {"#include \"\($0)\""} + result + ["\n"] -} - -class ResultObject: Equatable { - let path: Path - var settings: [String] - let configurationName: String - init(path: Path, settings: [String], configurationName: String) { - self.path = path - self.settings = settings - self.configurationName = configurationName - } -} -func ==(lhs: ResultObject, rhs: ResultObject) -> Bool { - guard lhs.path == rhs.path else { return false } - guard lhs.settings == rhs.settings else { return false } - guard lhs.configurationName == rhs.configurationName else { return false } - return true -} - let main = command( Argument("PATH", description: "xcodeproj file", validator: dirExists), Argument("DIR", description: "Output directory of xcconfig files. Mkdirs if missing. Files are overwritten."), Flag("no-trim-duplicates", description: "Don't extract duplicated lines to common xcconfig files, simply map each buildSettings to one file.", default: false), - Flag("no-edit-pbxproj", description: "Do not modify pbxproj.", default: false) -) { xcodeprojPath, dirPath, isNoTrimDuplicates, isNoEdit in + Flag("no-edit-pbxproj", description: "Do not modify pbxproj.", default: false), + Flag("include-existing", description: "`#include` already configured xcconfigs.", default: true) +) { xcodeprojPath, dirPath, isNoTrimDuplicates, isNoEdit, isIncludeExisting in let pbxprojPath = xcodeprojPath + Path("project.pbxproj") + guard pbxprojPath.isFile else { + printStdError("pbxproj not exist!: \(pbxprojPath.string)") + exit(1) + } + let projRoot = xcodeprojPath + ".." + // validate DIR + guard dirPath.absolute().components.starts(with: projRoot.absolute().components) else { + printStdError("Invalid DIR parameter: \(dirPath.string)\nIt must be descendant of xcodeproj's root dir: \(projRoot.string)") + exit(1) + } + + if dirPath.isFile { + printStdError("file already exists: \(dirPath.string)") + exit(1) + } if dirPath.isDirectory == false { try! dirPath.mkpath() } + // config + let config = Config(isIncludeExisting: isIncludeExisting) + let formatter = ResultFormatter(config: config) + // // read // let data: Data = try pbxprojPath.read() guard let pbxproj = Pbxproj(data: data) else { - fatalError("Failed to parse Pbxproj") + printStdError("Failed to parse Pbxproj") + exit(1) } // @@ -73,7 +69,15 @@ let main = command( let filePath = Path("\(dirPath.string)/\(configuration.name).xcconfig") let buildSettings = configuration.buildSettings let lines = convertToLines(buildSettings) - baseResults.append(ResultObject(path: filePath, settings: lines, configurationName: configuration.name)) + let r = ResultObject(path: filePath, settings: lines, configurationName: configuration.name) + if config.isIncludeExisting { + if let fileref = configuration.baseConfigurationReference { + let depth = (dirPath.components - projRoot.components).count + let prefix = (0..