diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d13a787f..30115b27e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Version +#### Added + +- Added ability to encode ProjectSpec to JSON [#545](https://github.com/yonaskolb/XcodeGen/pull/545) @ryohey + ## 2.5.0 #### Added diff --git a/Sources/ProjectSpec/AggregateTarget.swift b/Sources/ProjectSpec/AggregateTarget.swift index e25fb5532..d861958ff 100644 --- a/Sources/ProjectSpec/AggregateTarget.swift +++ b/Sources/ProjectSpec/AggregateTarget.swift @@ -62,6 +62,19 @@ extension AggregateTarget: NamedJSONDictionaryConvertible { } } +extension AggregateTarget: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "settings": settings.toJSONValue(), + "targets": targets, + "configFiles": configFiles, + "attributes": attributes, + "buildScripts": buildScripts.map { $0.toJSONValue() }, + "scheme": scheme?.toJSONValue() + ] as [String: Any?] + } +} + extension AggregateTarget: PathContainer { static var pathProperties: [PathProperty] { diff --git a/Sources/ProjectSpec/BuildRule.swift b/Sources/ProjectSpec/BuildRule.swift index 442dda248..3f75e75fd 100644 --- a/Sources/ProjectSpec/BuildRule.swift +++ b/Sources/ProjectSpec/BuildRule.swift @@ -80,3 +80,29 @@ extension BuildRule: JSONObjectConvertible { name = jsonDictionary.json(atKeyPath: "name") } } + +extension BuildRule: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "outputFiles": outputFiles, + "outputFilesCompilerFlags": outputFilesCompilerFlags, + "name": name + ] + + switch fileType { + case .pattern(let string): + dict["filePattern"] = string + case .type(let string): + dict["fileType"] = string + } + + switch action { + case .compilerSpec(let string): + dict["compilerSpec"] = string + case .script(let string): + dict["script"] = string + } + + return dict + } +} diff --git a/Sources/ProjectSpec/BuildScript.swift b/Sources/ProjectSpec/BuildScript.swift index 242a28133..f1eaf33a5 100644 --- a/Sources/ProjectSpec/BuildScript.swift +++ b/Sources/ProjectSpec/BuildScript.swift @@ -2,6 +2,8 @@ import Foundation import JSONUtilities public struct BuildScript: Equatable { + public static let runOnlyWhenInstallingDefault = false + public static let showEnvVarsDefault = true public var script: ScriptType public var name: String? @@ -26,8 +28,8 @@ public struct BuildScript: Equatable { inputFileLists: [String] = [], outputFileLists: [String] = [], shell: String? = nil, - runOnlyWhenInstalling: Bool = false, - showEnvVars: Bool = true + runOnlyWhenInstalling: Bool = runOnlyWhenInstallingDefault, + showEnvVars: Bool = showEnvVarsDefault ) { self.script = script self.name = name @@ -57,8 +59,34 @@ extension BuildScript: JSONObjectConvertible { script = .path(path) } shell = jsonDictionary.json(atKeyPath: "shell") - runOnlyWhenInstalling = jsonDictionary.json(atKeyPath: "runOnlyWhenInstalling") ?? false - showEnvVars = jsonDictionary.json(atKeyPath: "showEnvVars") ?? true + runOnlyWhenInstalling = jsonDictionary.json(atKeyPath: "runOnlyWhenInstalling") ?? BuildScript.runOnlyWhenInstallingDefault + showEnvVars = jsonDictionary.json(atKeyPath: "showEnvVars") ?? BuildScript.showEnvVarsDefault + } +} +extension BuildScript: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "inputFiles": inputFiles, + "inputFileLists": inputFileLists, + "outputFiles": outputFiles, + "outputFileLists": outputFileLists, + "runOnlyWhenInstalling": runOnlyWhenInstalling, + "name": name, + "shell": shell + ] + + if showEnvVars != BuildScript.showEnvVarsDefault { + dict["showEnvVars"] = showEnvVars + } + + switch script { + case .path(let string): + dict["path"] = string + case .script(let string): + dict["script"] = string + } + + return dict } } diff --git a/Sources/ProjectSpec/Dependency.swift b/Sources/ProjectSpec/Dependency.swift index d9495e441..8da36e6d6 100644 --- a/Sources/ProjectSpec/Dependency.swift +++ b/Sources/ProjectSpec/Dependency.swift @@ -2,15 +2,18 @@ import Foundation import JSONUtilities public struct Dependency: Equatable { + public static let removeHeadersDefault = true + public static let implicitDefault = false + public static let weakLinkDefault = false public var type: DependencyType public var reference: String public var embed: Bool? public var codeSign: Bool? - public var removeHeaders: Bool = true + public var removeHeaders: Bool = removeHeadersDefault public var link: Bool? - public var implicit: Bool = false - public var weakLink: Bool = false + public var implicit: Bool = implicitDefault + public var weakLink: Bool = weakLinkDefault public init( type: DependencyType, @@ -18,8 +21,8 @@ public struct Dependency: Equatable { embed: Bool? = nil, codeSign: Bool? = nil, link: Bool? = nil, - implicit: Bool = false, - weakLink: Bool = false + implicit: Bool = implicitDefault, + weakLink: Bool = weakLinkDefault ) { self.type = type self.reference = reference @@ -81,6 +84,42 @@ extension Dependency: JSONObjectConvertible { } } +extension Dependency: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "embed": embed, + "codeSign": codeSign, + "link": link + ] + + if removeHeaders != Dependency.removeHeadersDefault { + dict["removeHeaders"] = removeHeaders + } + if implicit != Dependency.implicitDefault { + dict["implicit"] = implicit + } + if weakLink != Dependency.weakLinkDefault { + dict["weak"] = weakLink + } + + switch type { + case .target: + dict["target"] = reference + case .framework: + dict["framework"] = reference + case .carthage(let findFrameworks): + dict["carthage"] = reference + if let findFrameworks = findFrameworks { + dict["findFrameworks"] = findFrameworks + } + case .sdk: + dict["sdk"] = reference + } + + return dict + } +} + extension Dependency: PathContainer { static var pathProperties: [PathProperty] { diff --git a/Sources/ProjectSpec/DeploymentTarget.swift b/Sources/ProjectSpec/DeploymentTarget.swift index cf797b020..7cba891a7 100644 --- a/Sources/ProjectSpec/DeploymentTarget.swift +++ b/Sources/ProjectSpec/DeploymentTarget.swift @@ -78,3 +78,14 @@ extension DeploymentTarget: JSONObjectConvertible { macOS = try parseVersion("macOS") } } + +extension DeploymentTarget: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "iOS": iOS?.string, + "tvOS": tvOS?.string, + "watchOS": watchOS?.string, + "macOS": macOS?.string, + ] + } +} diff --git a/Sources/ProjectSpec/Encoding.swift b/Sources/ProjectSpec/Encoding.swift new file mode 100644 index 000000000..6934f0b2d --- /dev/null +++ b/Sources/ProjectSpec/Encoding.swift @@ -0,0 +1,7 @@ +import Foundation +import JSONUtilities + +public protocol JSONEncodable { + // returns JSONDictionary or JSONArray or JSONRawType or nil + func toJSONValue() -> Any +} diff --git a/Sources/ProjectSpec/Plist.swift b/Sources/ProjectSpec/Plist.swift index 1f295106e..e174a9ab0 100644 --- a/Sources/ProjectSpec/Plist.swift +++ b/Sources/ProjectSpec/Plist.swift @@ -25,6 +25,15 @@ extension Plist: JSONObjectConvertible { } } +extension Plist: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "path": path, + "properties": properties + ] + } +} + extension Plist: PathContainer { static var pathProperties: [PathProperty] { diff --git a/Sources/ProjectSpec/Project.swift b/Sources/ProjectSpec/Project.swift index 7eb90873b..2790d0085 100644 --- a/Sources/ProjectSpec/Project.swift +++ b/Sources/ProjectSpec/Project.swift @@ -230,3 +230,31 @@ extension BuildSettingsContainer { return configFiles.values.map { Path($0) } } } + +extension Project: JSONEncodable { + public func toJSONValue() -> Any { + return toJSONDictionary() + } + + public func toJSONDictionary() -> JSONDictionary { + let targetPairs = targets.map { ($0.name, $0.toJSONValue()) } + let configsPairs = configs.map { ($0.name, $0.type?.rawValue) } + let aggregateTargetsPairs = aggregateTargets.map { ($0.name, $0.toJSONValue()) } + let schemesPairs = schemes.map { ($0.name, $0.toJSONValue()) } + + return [ + "name": name, + "options": options.toJSONValue(), + "settings": settings.toJSONValue(), + "fileGroups": fileGroups, + "configFiles": configFiles, + "include": include, + "attributes": attributes, + "targets": Dictionary(uniqueKeysWithValues: targetPairs), + "configs": Dictionary(uniqueKeysWithValues: configsPairs), + "aggregateTargets": Dictionary(uniqueKeysWithValues: aggregateTargetsPairs), + "schemes": Dictionary(uniqueKeysWithValues: schemesPairs), + "settingGroups": settingGroups.mapValues { $0.toJSONValue() } + ] + } +} diff --git a/Sources/ProjectSpec/Scheme.swift b/Sources/ProjectSpec/Scheme.swift index 6865c2844..58e6c56c9 100644 --- a/Sources/ProjectSpec/Scheme.swift +++ b/Sources/ProjectSpec/Scheme.swift @@ -44,6 +44,9 @@ public struct Scheme: Equatable { } public struct Build: Equatable { + public static let parallelizeBuildDefault = true + public static let buildImplicitDependenciesDefault = true + public var targets: [BuildTarget] public var parallelizeBuild: Bool public var buildImplicitDependencies: Bool @@ -51,8 +54,8 @@ public struct Scheme: Equatable { public var postActions: [ExecutionAction] public init( targets: [BuildTarget], - parallelizeBuild: Bool = true, - buildImplicitDependencies: Bool = true, + parallelizeBuild: Bool = parallelizeBuildDefault, + buildImplicitDependencies: Bool = buildImplicitDependenciesDefault, preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [] ) { @@ -86,6 +89,8 @@ public struct Scheme: Equatable { } public struct Test: BuildAction { + public static let gatherCoverageDataDefault = false + public var config: String? public var gatherCoverageData: Bool public var commandLineArguments: [String: Bool] @@ -95,14 +100,17 @@ public struct Scheme: Equatable { public var environmentVariables: [XCScheme.EnvironmentVariable] public struct TestTarget: Equatable, ExpressibleByStringLiteral { + public static let randomExecutionOrderDefault = false + public static let parallelizableDefault = false + public let name: String public var randomExecutionOrder: Bool public var parallelizable: Bool public init( name: String, - randomExecutionOrder: Bool = false, - parallelizable: Bool = false + randomExecutionOrder: Bool = randomExecutionOrderDefault, + parallelizable: Bool = parallelizableDefault ) { self.name = name self.randomExecutionOrder = randomExecutionOrder @@ -118,7 +126,7 @@ public struct Scheme: Equatable { public init( config: String, - gatherCoverageData: Bool = false, + gatherCoverageData: Bool = gatherCoverageDataDefault, randomExecutionOrder: Bool = false, parallelizable: Bool = false, commandLineArguments: [String: Bool] = [:], @@ -174,6 +182,8 @@ public struct Scheme: Equatable { } public struct Archive: BuildAction { + public static let revealArchiveInOrganizerDefault = true + public var config: String? public var customArchiveName: String? public var revealArchiveInOrganizer: Bool @@ -182,7 +192,7 @@ public struct Scheme: Equatable { public init( config: String, customArchiveName: String? = nil, - revealArchiveInOrganizer: Bool = true, + revealArchiveInOrganizer: Bool = revealArchiveInOrganizerDefault, preActions: [ExecutionAction] = [], postActions: [ExecutionAction] = [] ) { @@ -218,6 +228,16 @@ extension Scheme.ExecutionAction: JSONObjectConvertible { } } +extension Scheme.ExecutionAction: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "script": script, + "name": name, + "settingsTarget": settingsTarget + ] + } +} + extension Scheme.Run: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { @@ -229,11 +249,23 @@ extension Scheme.Run: JSONObjectConvertible { } } +extension Scheme.Run: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "commandLineArguments": commandLineArguments, + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + "environmentVariables": environmentVariables.map { $0.toJSONValue() }, + "config": config + ] as [String: Any?] + } +} + extension Scheme.Test: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { config = jsonDictionary.json(atKeyPath: "config") - gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? false + gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? Scheme.Test.gatherCoverageDataDefault commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:] if let targets = jsonDictionary["targets"] as? [Any] { self.targets = try targets.compactMap { target in @@ -254,12 +286,47 @@ extension Scheme.Test: JSONObjectConvertible { } } +extension Scheme.Test: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "gatherCoverageData": gatherCoverageData, + "commandLineArguments": commandLineArguments, + "targets": targets.map { $0.toJSONValue() }, + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + "environmentVariables": environmentVariables.map { $0.toJSONValue() }, + "config": config + ] as [String: Any?] + } +} + extension Scheme.Test.TestTarget: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { name = try jsonDictionary.json(atKeyPath: "name") - randomExecutionOrder = jsonDictionary.json(atKeyPath: "randomExecutionOrder") ?? false - parallelizable = jsonDictionary.json(atKeyPath: "parallelizable") ?? false + randomExecutionOrder = jsonDictionary.json(atKeyPath: "randomExecutionOrder") ?? Scheme.Test.TestTarget.randomExecutionOrderDefault + parallelizable = jsonDictionary.json(atKeyPath: "parallelizable") ?? Scheme.Test.TestTarget.parallelizableDefault + } +} + +extension Scheme.Test.TestTarget: JSONEncodable { + public func toJSONValue() -> Any { + if !randomExecutionOrder && !parallelizable { + return name + } + + var dict: JSONDictionary = [ + "name": name + ] + + if randomExecutionOrder != Scheme.Test.TestTarget.randomExecutionOrderDefault { + dict["randomExecutionOrder"] = randomExecutionOrder + } + if parallelizable != Scheme.Test.TestTarget.parallelizableDefault { + dict["parallelizable"] = parallelizable + } + + return dict } } @@ -274,6 +341,18 @@ extension Scheme.Profile: JSONObjectConvertible { } } +extension Scheme.Profile: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "commandLineArguments": commandLineArguments, + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + "environmentVariables": environmentVariables.map { $0.toJSONValue() }, + "config": config + ] as [String: Any?] + } +} + extension Scheme.Analyze: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { @@ -281,17 +360,42 @@ extension Scheme.Analyze: JSONObjectConvertible { } } +extension Scheme.Analyze: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "config": config + ] + } +} + extension Scheme.Archive: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { config = jsonDictionary.json(atKeyPath: "config") customArchiveName = jsonDictionary.json(atKeyPath: "customArchiveName") - revealArchiveInOrganizer = jsonDictionary.json(atKeyPath: "revealArchiveInOrganizer") ?? true + revealArchiveInOrganizer = jsonDictionary.json(atKeyPath: "revealArchiveInOrganizer") ?? Scheme.Archive.revealArchiveInOrganizerDefault preActions = jsonDictionary.json(atKeyPath: "preActions") ?? [] postActions = jsonDictionary.json(atKeyPath: "postActions") ?? [] } } +extension Scheme.Archive: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + "config": config, + "customArchiveName": customArchiveName, + ] + + if revealArchiveInOrganizer != Scheme.Archive.revealArchiveInOrganizerDefault { + dict["revealArchiveInOrganizer"] = revealArchiveInOrganizer + } + + return dict + } +} + extension Scheme: NamedJSONDictionaryConvertible { public init(name: String, jsonDictionary: JSONDictionary) throws { @@ -305,6 +409,19 @@ extension Scheme: NamedJSONDictionaryConvertible { } } +extension Scheme: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "build": build.toJSONValue(), + "run": run?.toJSONValue(), + "test": test?.toJSONValue(), + "analyze": analyze?.toJSONValue(), + "profile": profile?.toJSONValue(), + "archive": archive?.toJSONValue(), + ] as [String: Any?] + } +} + extension Scheme.Build: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { @@ -332,8 +449,29 @@ extension Scheme.Build: JSONObjectConvertible { self.targets = targets.sorted { $0.target < $1.target } preActions = try jsonDictionary.json(atKeyPath: "preActions")?.map(Scheme.ExecutionAction.init) ?? [] postActions = try jsonDictionary.json(atKeyPath: "postActions")?.map(Scheme.ExecutionAction.init) ?? [] - parallelizeBuild = jsonDictionary.json(atKeyPath: "parallelizeBuild") ?? true - buildImplicitDependencies = jsonDictionary.json(atKeyPath: "buildImplicitDependencies") ?? true + parallelizeBuild = jsonDictionary.json(atKeyPath: "parallelizeBuild") ?? Scheme.Build.parallelizeBuildDefault + buildImplicitDependencies = jsonDictionary.json(atKeyPath: "buildImplicitDependencies") ?? Scheme.Build.buildImplicitDependenciesDefault + } +} + +extension Scheme.Build: JSONEncodable { + public func toJSONValue() -> Any { + let targetPairs = targets.map { ($0.target, $0.buildTypes.map { $0.toJSONValue() }) } + + var dict: JSONDictionary = [ + "targets": Dictionary(uniqueKeysWithValues: targetPairs), + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + ] + + if parallelizeBuild != Scheme.Build.parallelizeBuildDefault { + dict["parallelizeBuild"] = parallelizeBuild + } + if buildImplicitDependencies != Scheme.Build.buildImplicitDependenciesDefault { + dict["buildImplicitDependencies"] = buildImplicitDependencies + } + + return dict } } @@ -357,7 +495,20 @@ extension BuildType: JSONPrimitiveConvertible { } } +extension BuildType: JSONEncodable { + public func toJSONValue() -> Any { + switch self { + case .testing: return "testing" + case .profiling: return "profiling" + case .running: return "running" + case .archiving: return "archiving" + case .analyzing: return "analyzing" + } + } +} + extension XCScheme.EnvironmentVariable: JSONObjectConvertible { + public static let enabledDefault = true private static func parseValue(_ value: Any) -> String { if let bool = value as? Bool { @@ -377,7 +528,7 @@ extension XCScheme.EnvironmentVariable: JSONObjectConvertible { value = try jsonDictionary.json(atKeyPath: "value") } let variable: String = try jsonDictionary.json(atKeyPath: "variable") - let enabled: Bool = jsonDictionary.json(atKeyPath: "isEnabled") ?? true + let enabled: Bool = jsonDictionary.json(atKeyPath: "isEnabled") ?? XCScheme.EnvironmentVariable.enabledDefault self.init(variable: variable, value: value, enabled: enabled) } @@ -393,3 +544,18 @@ extension XCScheme.EnvironmentVariable: JSONObjectConvertible { } } } + +extension XCScheme.EnvironmentVariable: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any] = [ + "variable": variable, + "value": value + ] + + if enabled != XCScheme.EnvironmentVariable.enabledDefault { + dict["isEnabled"] = enabled + } + + return dict + } +} diff --git a/Sources/ProjectSpec/Settings.swift b/Sources/ProjectSpec/Settings.swift index 95254dd66..c223ab9fa 100644 --- a/Sources/ProjectSpec/Settings.swift +++ b/Sources/ProjectSpec/Settings.swift @@ -105,3 +105,16 @@ public func += (lhs: inout BuildSettings, rhs: BuildSettings?) { guard let rhs = rhs else { return } lhs.merge(rhs) } + +extension Settings: JSONEncodable { + public func toJSONValue() -> Any { + if groups.count > 0 || configSettings.count > 0 { + return [ + "base": buildSettings, + "groups": groups, + "configs": configSettings.mapValues { $0.toJSONValue() } + ] + } + return buildSettings + } +} diff --git a/Sources/ProjectSpec/SpecOptions.swift b/Sources/ProjectSpec/SpecOptions.swift index 1abc8f426..fc55641ed 100644 --- a/Sources/ProjectSpec/SpecOptions.swift +++ b/Sources/ProjectSpec/SpecOptions.swift @@ -2,6 +2,12 @@ import Foundation import JSONUtilities public struct SpecOptions: Equatable { + public static let settingPresetsDefault = SettingPresets.all + public static let createIntermediateGroupsDefault = false + public static let transitivelyLinkDependenciesDefault = false + public static let groupSortPositionDefault = GroupSortPosition.bottom + public static let generateEmptyDirectoriesDefault = false + public static let findCarthageFrameworksDefault = false public var minimumXcodeGenVersion: Version? public var carthageBuildPath: String? @@ -62,9 +68,9 @@ public struct SpecOptions: Equatable { minimumXcodeGenVersion: Version? = nil, carthageBuildPath: String? = nil, carthageExecutablePath: String? = nil, - createIntermediateGroups: Bool = false, + createIntermediateGroups: Bool = createIntermediateGroupsDefault, bundleIdPrefix: String? = nil, - settingPresets: SettingPresets = .all, + settingPresets: SettingPresets = settingPresetsDefault, developmentLanguage: String? = nil, indentWidth: UInt? = nil, tabWidth: UInt? = nil, @@ -73,10 +79,10 @@ public struct SpecOptions: Equatable { deploymentTarget: DeploymentTarget = .init(), disabledValidations: [ValidationType] = [], defaultConfig: String? = nil, - transitivelyLinkDependencies: Bool = false, - groupSortPosition: GroupSortPosition = .bottom, - generateEmptyDirectories: Bool = false, - findCarthageFrameworks: Bool = false + transitivelyLinkDependencies: Bool = transitivelyLinkDependenciesDefault, + groupSortPosition: GroupSortPosition = groupSortPositionDefault, + generateEmptyDirectories: Bool = generateEmptyDirectoriesDefault, + findCarthageFrameworks: Bool = findCarthageFrameworksDefault ) { self.minimumXcodeGenVersion = minimumXcodeGenVersion self.carthageBuildPath = carthageBuildPath @@ -109,8 +115,8 @@ extension SpecOptions: JSONObjectConvertible { carthageBuildPath = jsonDictionary.json(atKeyPath: "carthageBuildPath") carthageExecutablePath = jsonDictionary.json(atKeyPath: "carthageExecutablePath") bundleIdPrefix = jsonDictionary.json(atKeyPath: "bundleIdPrefix") - settingPresets = jsonDictionary.json(atKeyPath: "settingPresets") ?? .all - createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups") ?? false + settingPresets = jsonDictionary.json(atKeyPath: "settingPresets") ?? SpecOptions.settingPresetsDefault + createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups") ?? SpecOptions.createIntermediateGroupsDefault developmentLanguage = jsonDictionary.json(atKeyPath: "developmentLanguage") usesTabs = jsonDictionary.json(atKeyPath: "usesTabs") xcodeVersion = jsonDictionary.json(atKeyPath: "xcodeVersion") @@ -119,10 +125,46 @@ extension SpecOptions: JSONObjectConvertible { deploymentTarget = jsonDictionary.json(atKeyPath: "deploymentTarget") ?? DeploymentTarget() disabledValidations = jsonDictionary.json(atKeyPath: "disabledValidations") ?? [] defaultConfig = jsonDictionary.json(atKeyPath: "defaultConfig") - transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies") ?? false - groupSortPosition = jsonDictionary.json(atKeyPath: "groupSortPosition") ?? .bottom - generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? false - findCarthageFrameworks = jsonDictionary.json(atKeyPath: "findCarthageFrameworks") ?? false + transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies") ?? SpecOptions.transitivelyLinkDependenciesDefault + groupSortPosition = jsonDictionary.json(atKeyPath: "groupSortPosition") ?? SpecOptions.groupSortPositionDefault + generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? SpecOptions.generateEmptyDirectoriesDefault + findCarthageFrameworks = jsonDictionary.json(atKeyPath: "findCarthageFrameworks") ?? SpecOptions.findCarthageFrameworksDefault + } +} + +extension SpecOptions: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "deploymentTarget": deploymentTarget.toJSONValue(), + "transitivelyLinkDependencies": transitivelyLinkDependencies, + "groupSortPosition": groupSortPosition.rawValue, + "disabledValidations": disabledValidations.map { $0.rawValue }, + "minimumXcodeGenVersion": minimumXcodeGenVersion?.string, + "carthageBuildPath": carthageBuildPath, + "carthageExecutablePath": carthageExecutablePath, + "bundleIdPrefix": bundleIdPrefix, + "developmentLanguage": developmentLanguage, + "usesTabs": usesTabs, + "xcodeVersion": xcodeVersion, + "indentWidth": indentWidth.flatMap { Int($0) }, + "tabWidth": tabWidth.flatMap { Int($0) }, + "defaultConfig": defaultConfig, + ] + + if settingPresets != SpecOptions.settingPresetsDefault { + dict["settingPresets"] = settingPresets.rawValue + } + if createIntermediateGroups != SpecOptions.createIntermediateGroupsDefault { + dict["createIntermediateGroups"] = createIntermediateGroups + } + if generateEmptyDirectories != SpecOptions.generateEmptyDirectoriesDefault { + dict["generateEmptyDirectories"] = generateEmptyDirectories + } + if findCarthageFrameworks != SpecOptions.findCarthageFrameworksDefault { + dict["findCarthageFrameworks"] = findCarthageFrameworks + } + + return dict } } diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index b7eb1c00a..403c5ed95 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -3,6 +3,8 @@ import JSONUtilities import xcodeproj public struct LegacyTarget: Equatable { + public static let passSettingsDefault = false + public var toolPath: String public var arguments: String? public var passSettings: Bool @@ -10,7 +12,7 @@ public struct LegacyTarget: Equatable { public init( toolPath: String, - passSettings: Bool = false, + passSettings: Bool = passSettingsDefault, arguments: String? = nil, workingDirectory: String? = nil ) { @@ -267,11 +269,27 @@ extension LegacyTarget: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { toolPath = try jsonDictionary.json(atKeyPath: "toolPath") arguments = jsonDictionary.json(atKeyPath: "arguments") - passSettings = jsonDictionary.json(atKeyPath: "passSettings") ?? false + passSettings = jsonDictionary.json(atKeyPath: "passSettings") ?? LegacyTarget.passSettingsDefault workingDirectory = jsonDictionary.json(atKeyPath: "workingDirectory") } } +extension LegacyTarget: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "toolPath": toolPath, + "arguments": arguments, + "workingDirectory": workingDirectory, + ] + + if passSettings != LegacyTarget.passSettingsDefault { + dict["passSettings"] = passSettings + } + + return dict + } +} + extension Target: NamedJSONDictionaryConvertible { public init(name: String, jsonDictionary: JSONDictionary) throws { @@ -342,3 +360,36 @@ extension Target: NamedJSONDictionaryConvertible { attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:] } } + + +extension Target: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "type": type.name, + "platform": platform.rawValue, + "settings": settings.toJSONValue(), + "configFiles": configFiles, + "attributes": attributes, + "sources": sources.map { $0.toJSONValue() }, + "dependencies": dependencies.map { $0.toJSONValue() }, + "postCompileScripts": postCompileScripts.map{ $0.toJSONValue() }, + "prebuildScripts": preBuildScripts.map{ $0.toJSONValue() }, + "postbuildScripts": postBuildScripts.map{ $0.toJSONValue() }, + "buildRules": buildRules.map{ $0.toJSONValue() }, + "deploymentTarget": deploymentTarget?.deploymentTarget, + "info": info?.toJSONValue(), + "entitlements": entitlements?.toJSONValue(), + "transitivelyLinkDependencies": transitivelyLinkDependencies, + "directlyEmbedCarthageDependencies": directlyEmbedCarthageDependencies, + "requiresObjCLinking": requiresObjCLinking, + "scheme": scheme?.toJSONValue(), + "legacy": legacy?.toJSONValue(), + ] + + if productName != name { + dict["productName"] = productName + } + + return dict + } +} diff --git a/Sources/ProjectSpec/TargetScheme.swift b/Sources/ProjectSpec/TargetScheme.swift index 22b018e9a..e059f6c56 100644 --- a/Sources/ProjectSpec/TargetScheme.swift +++ b/Sources/ProjectSpec/TargetScheme.swift @@ -3,6 +3,8 @@ import JSONUtilities import xcodeproj public struct TargetScheme: Equatable { + public static let gatherCoverageDataDefault = false + public var testTargets: [Scheme.Test.TestTarget] public var configVariants: [String] public var gatherCoverageData: Bool @@ -14,7 +16,7 @@ public struct TargetScheme: Equatable { public init( testTargets: [Scheme.Test.TestTarget] = [], configVariants: [String] = [], - gatherCoverageData: Bool = false, + gatherCoverageData: Bool = gatherCoverageDataDefault, commandLineArguments: [String: Bool] = [:], environmentVariables: [XCScheme.EnvironmentVariable] = [], preActions: [Scheme.ExecutionAction] = [], @@ -47,10 +49,29 @@ extension TargetScheme: JSONObjectConvertible { testTargets = [] } configVariants = jsonDictionary.json(atKeyPath: "configVariants") ?? [] - gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? false + gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? TargetScheme.gatherCoverageDataDefault commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:] environmentVariables = try XCScheme.EnvironmentVariable.parseAll(jsonDictionary: jsonDictionary) preActions = jsonDictionary.json(atKeyPath: "preActions") ?? [] postActions = jsonDictionary.json(atKeyPath: "postActions") ?? [] } } + +extension TargetScheme: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any] = [ + "configVariants": configVariants, + "commandLineArguments": commandLineArguments, + "testTargets": testTargets.map { $0.toJSONValue() }, + "environmentVariables": environmentVariables.map { $0.toJSONValue() }, + "preActions": preActions.map { $0.toJSONValue() }, + "postActions": postActions.map { $0.toJSONValue() }, + ] + + if gatherCoverageData != TargetScheme.gatherCoverageDataDefault { + dict["gatherCoverageData"] = gatherCoverageData + } + + return dict + } +} diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index 6ed21bff9..e21dbe781 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -4,6 +4,7 @@ import PathKit import xcodeproj public struct TargetSource: Equatable { + public static let optionalDefault = false public var path: String public var name: String? @@ -123,7 +124,7 @@ public struct TargetSource: Equatable { compilerFlags: [String] = [], excludes: [String] = [], type: SourceType? = nil, - optional: Bool = false, + optional: Bool = optionalDefault, buildPhase: BuildPhase? = nil, headerVisibility: HeaderVisibility? = nil, createIntermediateGroups: Bool? = nil @@ -169,7 +170,7 @@ extension TargetSource: JSONObjectConvertible { headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility") excludes = jsonDictionary.json(atKeyPath: "excludes") ?? [] type = jsonDictionary.json(atKeyPath: "type") - optional = jsonDictionary.json(atKeyPath: "optional") ?? false + optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") { buildPhase = try BuildPhase(string: string) @@ -181,6 +182,32 @@ extension TargetSource: JSONObjectConvertible { } } +extension TargetSource: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "compilerFlags": compilerFlags, + "excludes": excludes, + "name": name, + "headerVisibility": headerVisibility?.rawValue, + "type": type?.rawValue, + "buildPhase": buildPhase?.toJSONValue(), + "createIntermediateGroups": createIntermediateGroups, + ] + + if optional != TargetSource.optionalDefault { + dict["optional"] = optional + } + + if dict.count == 0 { + return path + } + + dict["path"] = path + + return dict + } +} + extension TargetSource.BuildPhase { public init(string: String) throws { @@ -204,6 +231,21 @@ extension TargetSource.BuildPhase: JSONObjectConvertible { } } +extension TargetSource.BuildPhase: JSONEncodable { + public func toJSONValue() -> Any { + switch self { + case .sources: return "sources" + case .headers: return "headers" + case .resources: return "resources" + case .copyFiles(let files): return ["copyFiles": files.toJSONValue()] + case .none: return "none" + case .frameworks: fatalError("invalid build phase") + case .runScript: fatalError("invalid build phase") + case .carbonResources: fatalError("invalid build phase") + } + } +} + extension TargetSource.BuildPhase.CopyFilesSettings: JSONObjectConvertible { public init(jsonDictionary: JSONDictionary) throws { @@ -213,6 +255,15 @@ extension TargetSource.BuildPhase.CopyFilesSettings: JSONObjectConvertible { } } +extension TargetSource.BuildPhase.CopyFilesSettings: JSONEncodable { + public func toJSONValue() -> Any { + return [ + "destination": destination.rawValue, + "subpath": subpath + ] + } +} + extension TargetSource: PathContainer { static var pathProperties: [PathProperty] { diff --git a/Tests/XcodeGenKitTests/ProjectSpecTests.swift b/Tests/XcodeGenKitTests/ProjectSpecTests.swift index 9fa496c60..33aaeba64 100644 --- a/Tests/XcodeGenKitTests/ProjectSpecTests.swift +++ b/Tests/XcodeGenKitTests/ProjectSpecTests.swift @@ -256,6 +256,248 @@ class ProjectSpecTests: XCTestCase { } } } + + func testJSONEncodable() { + describe { + $0.it("encodes to json") { + let proj = Project(basePath: Path.current, + name: "ToJson", + configs: [Config(name: "DevelopmentConfig", type: .debug), Config(name: "ProductionConfig", type: .release)], + targets: [Target(name: "App", + type: .application, + platform: .iOS, + productName: "App", + deploymentTarget: Version(major: 0, minor: 1, patch: 2), + settings: Settings(buildSettings: ["foo": "bar"], + configSettings: ["foo" : Settings(buildSettings: ["nested": "config"], + configSettings: [:], + groups: ["config-setting-group"])], + groups: ["setting-group"]), + configFiles: ["foo": "bar"], + sources: [TargetSource(path: "Source", + name: "Source", + compilerFlags: ["-Werror"], + excludes: ["foo", "bar"], + type: .folder, + optional: true, + buildPhase: .resources, + headerVisibility: .private, + createIntermediateGroups: true)], + dependencies: [Dependency(type: .carthage(findFrameworks: true), + reference: "reference", + embed: true, + codeSign: true, + link: true, + implicit: true, + weakLink: true)], + info: Plist(path: "info.plist", attributes: ["foo": "bar"]), + entitlements: Plist(path: "entitlements.plist", attributes: ["foo": "bar"]), + transitivelyLinkDependencies: true, + directlyEmbedCarthageDependencies: true, + requiresObjCLinking: true, + preBuildScripts: [BuildScript(script: .script("pwd"), + name: "Foo script", + inputFiles: ["foo"], + outputFiles: ["bar"], + inputFileLists: ["foo.xcfilelist"], + outputFileLists: ["bar.xcfilelist"], + shell: "/bin/bash", + runOnlyWhenInstalling: true, + showEnvVars: true)], + postCompileScripts: [BuildScript(script: .path("cmd.sh"), + name: "Bar script", + inputFiles: ["foo"], + outputFiles: ["bar"], + inputFileLists: ["foo.xcfilelist"], + outputFileLists: ["bar.xcfilelist"], + shell: "/bin/bash", + runOnlyWhenInstalling: true, + showEnvVars: true)], + postBuildScripts: [BuildScript(script: .path("cmd.sh"), + name: "an another script", + inputFiles: ["foo"], + outputFiles: ["bar"], + inputFileLists: ["foo.xcfilelist"], + outputFileLists: ["bar.xcfilelist"], + shell: "/bin/bash", + runOnlyWhenInstalling: true, + showEnvVars: true)], + buildRules: [BuildRule(fileType: .pattern("*.xcassets"), + action: .script("pre_process_swift.py"), + name: "My Build Rule", + outputFiles: ["$(SRCROOT)/Generated.swift"], + outputFilesCompilerFlags: ["foo"]), + BuildRule(fileType: .type("sourcecode.swift"), + action: .compilerSpec("com.apple.xcode.tools.swift.compiler"), + name: nil, + outputFiles: ["bar"], + outputFilesCompilerFlags: ["foo"]),], + scheme: TargetScheme(testTargets: [Scheme.Test.TestTarget(name: "test target", + randomExecutionOrder: false, + parallelizable: false)], + configVariants: ["foo"], + gatherCoverageData: true, + commandLineArguments: ["foo": true], + environmentVariables: [XCScheme.EnvironmentVariable(variable: "environmentVariable", + value: "bar", + enabled: true)], + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")]), + legacy: LegacyTarget(toolPath: "foo", + passSettings: true, + arguments: "bar", + workingDirectory: "foo"), + attributes: ["foo": "bar"])], + aggregateTargets: [AggregateTarget(name: "aggregate target", + targets: ["App"], + settings: Settings(buildSettings: ["buildSettings": "bar"], + configSettings: ["configSettings": Settings(buildSettings: [:], + configSettings: [:], + groups: [])], + groups: ["foo"]), + configFiles: ["configFiles": "bar"], + buildScripts: [BuildScript(script: .path("script"), + name: "foo", + inputFiles: ["foo"], + outputFiles: ["bar"], + inputFileLists: ["foo.xcfilelist"], + outputFileLists: ["bar.xcfilelist"], + shell: "/bin/bash", + runOnlyWhenInstalling: true, + showEnvVars: false)], + scheme: TargetScheme(testTargets: [Scheme.Test.TestTarget(name: "test target", + randomExecutionOrder: false, + parallelizable: false)], + configVariants: ["foo"], + gatherCoverageData: true, + commandLineArguments: ["foo": true], + environmentVariables: [XCScheme.EnvironmentVariable(variable: "environmentVariable", + value: "bar", + enabled: true)], + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")]), + attributes: ["foo": "bar"])], + settings: Settings(buildSettings: ["foo": "bar"], + configSettings: ["foo" : Settings(buildSettings: ["nested": "config"], + configSettings: [:], + groups: ["config-setting-group"])], + groups: ["setting-group"]), + settingGroups: ["foo": Settings(buildSettings: ["foo": "bar"], + configSettings: ["foo" : Settings(buildSettings: ["nested": "config"], + configSettings: [:], + groups: ["config-setting-group"])], + groups: ["setting-group"])], + schemes: [Scheme(name: "scheme", + build: Scheme.Build(targets: [Scheme.BuildTarget(target: "foo", + buildTypes: [.archiving, .analyzing])], + parallelizeBuild: false, + buildImplicitDependencies: false, + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")]), + run: Scheme.Run(config: "run config", + commandLineArguments: ["foo": true], + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")], + environmentVariables: [XCScheme.EnvironmentVariable(variable: "foo", + value: "bar", + enabled: false)]), + test: Scheme.Test(config: "Config", + gatherCoverageData: true, + randomExecutionOrder: false, + parallelizable: false, + commandLineArguments: ["foo": true], + targets: [Scheme.Test.TestTarget(name: "foo", + randomExecutionOrder: false, + parallelizable: false)], + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")], + environmentVariables: [XCScheme.EnvironmentVariable(variable: "foo", + value: "bar", + enabled: false)]), + profile: Scheme.Profile(config: "profile config", + commandLineArguments: ["foo": true], + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")], + environmentVariables: [XCScheme.EnvironmentVariable(variable: "foo", + value: "bar", + enabled: false)]), + analyze: Scheme.Analyze(config: "analyze config"), + archive: Scheme.Archive(config: "archive config", + customArchiveName: "customArchiveName", + revealArchiveInOrganizer: true, + preActions: [Scheme.ExecutionAction(name: "preAction", + script: "bar", + settingsTarget: "foo")], + postActions: [Scheme.ExecutionAction(name: "postAction", + script: "bar", + settingsTarget: "foo")]))], + options: SpecOptions(minimumXcodeGenVersion: Version(major: 3, minor: 4, patch: 5), + carthageBuildPath: "carthageBuildPath", + carthageExecutablePath: "carthageExecutablePath", + createIntermediateGroups: true, + bundleIdPrefix: "bundleIdPrefix", + settingPresets: .project, + developmentLanguage: "developmentLanguage", + indentWidth: 123, + tabWidth: 456, + usesTabs: true, + xcodeVersion: "xcodeVersion", + deploymentTarget: DeploymentTarget(iOS: Version(major: 1, minor: 2, patch: 3), + tvOS: nil, + watchOS: Version(major: 4, minor: 5, patch: 6), + macOS: nil), + disabledValidations: [.missingConfigFiles], + defaultConfig: "defaultConfig", + transitivelyLinkDependencies: true, + groupSortPosition: .top, + generateEmptyDirectories: true, + findCarthageFrameworks: false), + fileGroups: ["foo", "bar"], + configFiles: ["configFiles": "bar"], + attributes: ["attributes": "bar"]) + + let json = proj.toJSONDictionary() + let restoredProj = try Project(basePath: Path.current, jsonDictionary: json) + + // Examin some properties to make debugging easier + try expect(proj.aggregateTargets) == restoredProj.aggregateTargets + try expect(proj.configFiles) == restoredProj.configFiles + try expect(proj.settings) == restoredProj.settings + try expect(proj.basePath) == restoredProj.basePath + try expect(proj.fileGroups) == restoredProj.fileGroups + try expect(proj.schemes) == restoredProj.schemes + try expect(proj.options) == restoredProj.options + try expect(proj.settingGroups) == restoredProj.settingGroups + try expect(proj.targets) == restoredProj.targets + + try expect(proj) == restoredProj + } + } + } } fileprivate func expectValidationError(_ project: Project, _ expectedError: SpecValidationError.ValidationError, file: String = #file, line: Int = #line) throws {