GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,96 @@
import Foundation
import JSONUtilities
import XcodeProj
public struct AggregateTarget: ProjectTarget {
public var name: String
public var type: PBXProductType = .none
public var targets: [String]
public var settings: Settings
public var buildScripts: [BuildScript]
public var buildToolPlugins: [BuildToolPlugin]
public var configFiles: [String: String]
public var scheme: TargetScheme?
public var attributes: [String: Any]
public init(
name: String,
targets: [String],
settings: Settings = .empty,
configFiles: [String: String] = [:],
buildScripts: [BuildScript] = [],
buildToolPlugins: [BuildToolPlugin] = [],
scheme: TargetScheme? = nil,
attributes: [String: Any] = [:]
) {
self.name = name
self.targets = targets
self.settings = settings
self.configFiles = configFiles
self.buildScripts = buildScripts
self.buildToolPlugins = buildToolPlugins
self.scheme = scheme
self.attributes = attributes
}
}
extension AggregateTarget: CustomStringConvertible {
public var description: String {
"\(name)\(targets.isEmpty ? "" : ": \(targets.joined(separator: ", "))")"
}
}
extension AggregateTarget: Equatable {
public static func == (lhs: AggregateTarget, rhs: AggregateTarget) -> Bool {
lhs.name == rhs.name &&
lhs.targets == rhs.targets &&
lhs.settings == rhs.settings &&
lhs.configFiles == rhs.configFiles &&
lhs.buildScripts == rhs.buildScripts &&
lhs.buildToolPlugins == rhs.buildToolPlugins &&
lhs.scheme == rhs.scheme &&
NSDictionary(dictionary: lhs.attributes).isEqual(to: rhs.attributes)
}
}
extension AggregateTarget: NamedJSONDictionaryConvertible {
public init(name: String, jsonDictionary: JSONDictionary) throws {
self.name = jsonDictionary.json(atKeyPath: "name") ?? name
targets = jsonDictionary.json(atKeyPath: "targets") ?? []
settings = try BuildSettingsParser(jsonDictionary: jsonDictionary).parse()
configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:]
buildScripts = jsonDictionary.json(atKeyPath: "buildScripts") ?? []
buildToolPlugins = jsonDictionary.json(atKeyPath: "buildToolPlugins") ?? []
scheme = jsonDictionary.json(atKeyPath: "scheme")
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
}
}
extension AggregateTarget: JSONEncodable {
public func toJSONValue() -> Any {
[
"settings": settings.toJSONValue(),
"targets": targets,
"configFiles": configFiles,
"attributes": attributes,
"buildScripts": buildScripts.map { $0.toJSONValue() },
"buildToolPlugins": buildToolPlugins.map { $0.toJSONValue() },
"scheme": scheme?.toJSONValue(),
] as [String: Any?]
}
}
extension AggregateTarget: PathContainer {
static var pathProperties: [PathProperty] {
[
.dictionary([
.string("configFiles"),
.object("buildScripts", BuildScript.pathProperties),
]),
]
}
}
@@ -0,0 +1,9 @@
extension Array where Element == [String: Any?] {
func removingEmptyArraysDictionariesAndNils() -> [[String: Any]] {
var new: [[String: Any]] = []
forEach { element in
new.append(element.removingEmptyArraysDictionariesAndNils())
}
return new
}
}
@@ -0,0 +1,264 @@
import Foundation
import XcodeProj
import JSONUtilities
public typealias BreakpointActionExtensionID = XCBreakpointList.BreakpointProxy.BreakpointContent.BreakpointActionProxy.ActionExtensionID
public typealias BreakpointExtensionID = XCBreakpointList.BreakpointProxy.BreakpointExtensionID
public struct Breakpoint: Equatable {
public enum BreakpointType: Equatable {
public struct Exception: Equatable {
public enum Scope: String, Equatable {
case all = "0"
case objectiveC = "1"
case cpp = "2"
}
public enum StopOnStyle: String, Equatable {
case `throw` = "0"
case `catch` = "1"
}
public var scope: Scope
public var stopOnStyle: StopOnStyle
public init(scope: Breakpoint.BreakpointType.Exception.Scope = .objectiveC,
stopOnStyle: Breakpoint.BreakpointType.Exception.StopOnStyle = .throw) {
self.scope = scope
self.stopOnStyle = stopOnStyle
}
}
case file(path: String, line: Int, column: Int?)
case exception(Exception)
case swiftError
case openGLError
case symbolic(symbol: String?, module: String?)
case ideConstraintError
case ideTestFailure
case runtimeIssue
}
public enum Action: Equatable {
public struct Log: Equatable {
public enum ConveyanceType: String, Equatable {
case console = "0"
case speak = "1"
}
public var message: String?
public var conveyanceType: ConveyanceType
public init(message: String? = nil, conveyanceType: Breakpoint.Action.Log.ConveyanceType = .console) {
self.message = message
self.conveyanceType = conveyanceType
}
}
public enum Sound: String, Equatable {
case basso = "Basso"
case blow = "Blow"
case bottle = "Bottle"
case frog = "Frog"
case funk = "Funk"
case glass = "Glass"
case hero = "Hero"
case morse = "Morse"
case ping = "Ping"
case pop = "Pop"
case purr = "Purr"
case sosumi = "Sosumi"
case submarine = "Submarine"
case tink = "Tink"
}
case debuggerCommand(String?)
case log(Log)
case shellCommand(path: String?, arguments: String?, waitUntilDone: Bool = false)
case graphicsTrace
case appleScript(String?)
case sound(Sound)
}
public var type: BreakpointType
public var enabled: Bool
public var ignoreCount: Int
public var continueAfterRunningActions: Bool
public var condition: String?
public var actions: [Breakpoint.Action]
public init(type: BreakpointType,
enabled: Bool = true,
ignoreCount: Int = 0,
continueAfterRunningActions: Bool = false,
filePath: String? = nil,
line: Int? = nil,
condition: String? = nil,
actions: [Breakpoint.Action] = []) {
self.type = type
self.enabled = enabled
self.ignoreCount = ignoreCount
self.continueAfterRunningActions = continueAfterRunningActions
self.condition = condition
self.actions = actions
}
}
extension Breakpoint.BreakpointType.Exception.Scope {
public init(string: String) throws {
let string = string.lowercased()
switch string {
case "all":
self = .all
case "objective-c":
self = .objectiveC
case "c++":
self = .cpp
default:
throw SpecParsingError.unknownBreakpointScope(string)
}
}
}
extension Breakpoint.BreakpointType.Exception.StopOnStyle {
public init(string: String) throws {
let string = string.lowercased()
switch string {
case "throw":
self = .throw
case "catch":
self = .catch
default:
throw SpecParsingError.unknownBreakpointStopOnStyle(string)
}
}
}
extension Breakpoint.Action.Log.ConveyanceType {
init(string: String) throws {
let string = string.lowercased()
switch string {
case "console":
self = .console
case "speak":
self = .speak
default:
throw SpecParsingError.unknownBreakpointActionConveyanceType(string)
}
}
}
extension Breakpoint.Action.Sound {
init(name: String) throws {
guard let sound = Self.init(rawValue: name) else {
throw SpecParsingError.unknownBreakpointActionSoundName(name)
}
self = sound
}
}
extension Breakpoint.Action: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
let idString: String = try jsonDictionary.json(atKeyPath: "type")
let id = try BreakpointActionExtensionID(string: idString)
switch id {
case .debuggerCommand:
let command: String? = jsonDictionary.json(atKeyPath: "command")
self = .debuggerCommand(command)
case .log:
let message: String? = jsonDictionary.json(atKeyPath: "message")
let conveyanceType: Log.ConveyanceType
if jsonDictionary["conveyanceType"] != nil {
let conveyanceTypeString: String = try jsonDictionary.json(atKeyPath: "conveyanceType")
conveyanceType = try .init(string: conveyanceTypeString)
} else {
conveyanceType = .console
}
self = .log(.init(message: message, conveyanceType: conveyanceType))
case .shellCommand:
let path: String? = jsonDictionary.json(atKeyPath: "path")
let arguments: String? = jsonDictionary.json(atKeyPath: "arguments")
let waitUntilDone = jsonDictionary.json(atKeyPath: "waitUntilDone") ?? false
self = .shellCommand(path: path, arguments: arguments, waitUntilDone: waitUntilDone)
case .graphicsTrace:
self = .graphicsTrace
case .appleScript:
let script: String? = jsonDictionary.json(atKeyPath: "script")
self = .appleScript(script)
case .sound:
let sound: Sound
if jsonDictionary["sound"] != nil {
let name: String = try jsonDictionary.json(atKeyPath: "sound")
sound = try .init(name: name)
} else {
sound = .basso
}
self = .sound(sound)
case .openGLError:
throw SpecParsingError.unknownBreakpointActionType(idString)
}
}
}
extension Breakpoint: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
let idString: String = try jsonDictionary.json(atKeyPath: "type")
let id = try BreakpointExtensionID(string: idString)
switch id {
case .file:
let path: String = try jsonDictionary.json(atKeyPath: "path")
let line: Int = try jsonDictionary.json(atKeyPath: "line")
let column: Int? = jsonDictionary.json(atKeyPath: "column")
type = .file(path: path, line: line, column: column)
case .exception:
let scope: BreakpointType.Exception.Scope
if jsonDictionary["scope"] != nil {
let scopeString: String = try jsonDictionary.json(atKeyPath: "scope")
scope = try .init(string: scopeString)
} else {
scope = .objectiveC
}
let stopOnStyle: BreakpointType.Exception.StopOnStyle
if jsonDictionary["stopOnStyle"] != nil {
let stopOnStyleString: String = try jsonDictionary.json(atKeyPath: "stopOnStyle")
stopOnStyle = try .init(string: stopOnStyleString)
} else {
stopOnStyle = .throw
}
type = .exception(.init(scope: scope, stopOnStyle: stopOnStyle))
case .swiftError:
type = .swiftError
case .openGLError:
type = .openGLError
case .symbolic:
let symbol: String? = jsonDictionary.json(atKeyPath: "symbol")
let module: String? = jsonDictionary.json(atKeyPath: "module")
type = .symbolic(symbol: symbol, module: module)
case .ideConstraintError:
type = .ideConstraintError
case .ideTestFailure:
type = .ideTestFailure
case .runtimeIssue:
type = .runtimeIssue
}
enabled = jsonDictionary.json(atKeyPath: "enabled") ?? true
ignoreCount = jsonDictionary.json(atKeyPath: "ignoreCount") ?? 0
continueAfterRunningActions = jsonDictionary.json(atKeyPath: "continueAfterRunningActions") ?? false
condition = jsonDictionary.json(atKeyPath: "condition")
if jsonDictionary["actions"] != nil {
actions = try jsonDictionary.json(atKeyPath: "actions", invalidItemBehaviour: .fail)
} else {
actions = []
}
}
}
@@ -0,0 +1,154 @@
//
// File.swift
//
//
// Created by Yonas Kolb on 1/5/20.
//
import Foundation
import XcodeProj
import JSONUtilities
public enum BuildPhaseSpec: Equatable {
case sources
case headers
case resources
case copyFiles(CopyFilesSettings)
case none
// Not currently exposed as selectable options, but used internally
case frameworks
case runScript
case carbonResources
public struct CopyFilesSettings: Equatable, Hashable {
public static let xpcServices = CopyFilesSettings(
destination: .productsDirectory,
subpath: "$(CONTENTS_FOLDER_PATH)/XPCServices",
phaseOrder: .postCompile
)
public static let plugins = CopyFilesSettings(
destination: .plugins,
subpath: "$(CONTENTS_FOLDER_PATH)/PlugIns",
phaseOrder: .postCompile
)
public enum Destination: String {
case absolutePath
case productsDirectory
case wrapper
case executables
case resources
case javaResources
case frameworks
case sharedFrameworks
case sharedSupport
case plugins
public var destination: PBXCopyFilesBuildPhase.SubFolder? {
switch self {
case .absolutePath: return .absolutePath
case .productsDirectory: return .productsDirectory
case .wrapper: return .wrapper
case .executables: return .executables
case .resources: return .resources
case .javaResources: return .javaResources
case .frameworks: return .frameworks
case .sharedFrameworks: return .sharedFrameworks
case .sharedSupport: return .sharedSupport
case .plugins: return .plugins
}
}
}
public enum PhaseOrder: String {
/// Run before the Compile Sources phase
case preCompile
/// Run after the Compile Sources and post-compile Run Script phases
case postCompile
}
public var destination: Destination
public var subpath: String
public var phaseOrder: PhaseOrder
public init(
destination: Destination,
subpath: String,
phaseOrder: PhaseOrder
) {
self.destination = destination
self.subpath = subpath
self.phaseOrder = phaseOrder
}
}
public var buildPhase: BuildPhase? {
switch self {
case .sources: return .sources
case .headers: return .headers
case .resources: return .resources
case .copyFiles: return .copyFiles
case .frameworks: return .frameworks
case .runScript: return .runScript
case .carbonResources: return .carbonResources
case .none: return nil
}
}
}
extension BuildPhaseSpec {
public init(string: String) throws {
switch string {
case "sources": self = .sources
case "headers": self = .headers
case "resources": self = .resources
case "copyFiles":
throw SpecParsingError.invalidSourceBuildPhase("copyFiles must specify a \"destination\" and optional \"subpath\"")
case "none": self = .none
default:
throw SpecParsingError.invalidSourceBuildPhase(string.quoted)
}
}
}
extension BuildPhaseSpec: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
self = .copyFiles(try jsonDictionary.json(atKeyPath: "copyFiles"))
}
}
extension BuildPhaseSpec: 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 BuildPhaseSpec.CopyFilesSettings: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
destination = try jsonDictionary.json(atKeyPath: "destination")
subpath = jsonDictionary.json(atKeyPath: "subpath") ?? ""
phaseOrder = .postCompile
}
}
extension BuildPhaseSpec.CopyFilesSettings: JSONEncodable {
public func toJSONValue() -> Any {
[
"destination": destination.rawValue,
"subpath": subpath,
]
}
}
+123
View File
@@ -0,0 +1,123 @@
import Foundation
import JSONUtilities
public struct BuildRule: Equatable {
public static let scriptCompilerSpec = "com.apple.compilers.proxy.script"
public static let filePatternFileType = "pattern.proxy"
public static let runOncePerArchitectureDefault = true
public enum FileType: Equatable {
case type(String)
case pattern(String)
public var fileType: String {
switch self {
case let .type(fileType): return fileType
case .pattern: return BuildRule.filePatternFileType
}
}
public var pattern: String? {
switch self {
case .type: return nil
case let .pattern(pattern): return pattern
}
}
}
public enum Action: Equatable {
case compilerSpec(String)
case script(String)
public var compilerSpec: String {
switch self {
case let .compilerSpec(compilerSpec): return compilerSpec
case .script: return BuildRule.scriptCompilerSpec
}
}
public var script: String? {
switch self {
case .compilerSpec: return nil
case let .script(script): return script
}
}
}
public var fileType: FileType
public var action: Action
public var outputFiles: [String]
public var outputFilesCompilerFlags: [String]
public var name: String?
public var runOncePerArchitecture: Bool
public init(
fileType: FileType,
action: Action,
name: String? = nil,
outputFiles: [String] = [],
outputFilesCompilerFlags: [String] = [],
runOncePerArchitecture: Bool = runOncePerArchitectureDefault
) {
self.fileType = fileType
self.action = action
self.name = name
self.outputFiles = outputFiles
self.outputFilesCompilerFlags = outputFilesCompilerFlags
self.runOncePerArchitecture = runOncePerArchitecture
}
}
extension BuildRule: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let fileType: String = jsonDictionary.json(atKeyPath: "fileType") {
self.fileType = .type(fileType)
} else {
fileType = .pattern(try jsonDictionary.json(atKeyPath: "filePattern"))
}
if let compilerSpec: String = jsonDictionary.json(atKeyPath: "compilerSpec") {
action = .compilerSpec(compilerSpec)
} else {
action = .script(try jsonDictionary.json(atKeyPath: "script"))
}
outputFiles = jsonDictionary.json(atKeyPath: "outputFiles") ?? []
outputFilesCompilerFlags = jsonDictionary.json(atKeyPath: "outputFilesCompilerFlags") ?? []
name = jsonDictionary.json(atKeyPath: "name")
runOncePerArchitecture = jsonDictionary.json(atKeyPath: "runOncePerArchitecture") ?? BuildRule.runOncePerArchitectureDefault
}
}
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
}
if runOncePerArchitecture != BuildRule.runOncePerArchitectureDefault {
dict["runOncePerArchitecture"] = runOncePerArchitecture
}
return dict
}
}
@@ -0,0 +1,118 @@
import Foundation
import JSONUtilities
public struct BuildScript: Equatable {
public static let runOnlyWhenInstallingDefault = false
public static let showEnvVarsDefault = true
public static let basedOnDependencyAnalysisDefault = true
public var script: ScriptType
public var name: String?
public var shell: String?
public var inputFiles: [String]
public var outputFiles: [String]
public var inputFileLists: [String]
public var outputFileLists: [String]
public var runOnlyWhenInstalling: Bool
public let showEnvVars: Bool
public let basedOnDependencyAnalysis: Bool
public let discoveredDependencyFile: String?
public enum ScriptType: Equatable {
case path(String)
case script(String)
}
public init(
script: ScriptType,
name: String? = nil,
inputFiles: [String] = [],
outputFiles: [String] = [],
inputFileLists: [String] = [],
outputFileLists: [String] = [],
shell: String? = nil,
runOnlyWhenInstalling: Bool = runOnlyWhenInstallingDefault,
showEnvVars: Bool = showEnvVarsDefault,
basedOnDependencyAnalysis: Bool = basedOnDependencyAnalysisDefault,
discoveredDependencyFile: String? = nil
) {
self.script = script
self.name = name
self.inputFiles = inputFiles
self.outputFiles = outputFiles
self.inputFileLists = inputFileLists
self.outputFileLists = outputFileLists
self.shell = shell
self.runOnlyWhenInstalling = runOnlyWhenInstalling
self.showEnvVars = showEnvVars
self.basedOnDependencyAnalysis = basedOnDependencyAnalysis
self.discoveredDependencyFile = discoveredDependencyFile
}
}
extension BuildScript: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
name = jsonDictionary.json(atKeyPath: "name")
inputFiles = jsonDictionary.json(atKeyPath: "inputFiles") ?? []
outputFiles = jsonDictionary.json(atKeyPath: "outputFiles") ?? []
inputFileLists = jsonDictionary.json(atKeyPath: "inputFileLists") ?? []
outputFileLists = jsonDictionary.json(atKeyPath: "outputFileLists") ?? []
if let string: String = jsonDictionary.json(atKeyPath: "script") {
script = .script(string)
} else {
let path: String = try jsonDictionary.json(atKeyPath: "path")
script = .path(path)
}
shell = jsonDictionary.json(atKeyPath: "shell")
runOnlyWhenInstalling = jsonDictionary.json(atKeyPath: "runOnlyWhenInstalling") ?? BuildScript.runOnlyWhenInstallingDefault
showEnvVars = jsonDictionary.json(atKeyPath: "showEnvVars") ?? BuildScript.showEnvVarsDefault
basedOnDependencyAnalysis = jsonDictionary.json(atKeyPath: "basedOnDependencyAnalysis") ?? BuildScript.basedOnDependencyAnalysisDefault
discoveredDependencyFile = jsonDictionary.json(atKeyPath: "discoveredDependencyFile")
}
}
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
}
if basedOnDependencyAnalysis != BuildScript.basedOnDependencyAnalysisDefault {
dict["basedOnDependencyAnalysis"] = basedOnDependencyAnalysis
}
switch script {
case .path(let string):
dict["path"] = string
case .script(let string):
dict["script"] = string
}
if let discoveredDependencyFile = discoveredDependencyFile {
dict["discoveredDependencyFile"] = discoveredDependencyFile
}
return dict
}
}
extension BuildScript: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("path"),
]
}
}
@@ -0,0 +1,7 @@
import Foundation
public protocol BuildSettingsContainer {
var settings: Settings { get }
var configFiles: [String: String] { get }
}
@@ -0,0 +1,37 @@
import Foundation
import JSONUtilities
/// A helper for extracting and validating the `Settings` object from a JSON dictionary.
struct BuildSettingsParser {
let jsonDictionary: JSONDictionary
/// Attempts to extract and parse the `Settings` from the dictionary.
///
/// - Returns: A valid `Settings` object
func parse() throws -> Settings {
do {
return try jsonDictionary.json(atKeyPath: "settings")
} catch let specParsingError as SpecParsingError {
// Re-throw `SpecParsingError` to prevent the misuse of settings.configs.
throw specParsingError
} catch {
// Ignore all errors except `SpecParsingError`
return .empty
}
}
/// Attempts to extract and parse setting groups from the dictionary with fallback defaults.
///
/// - Returns: Parsed setting groups or default groups if parsing fails
func parseSettingGroups() throws -> [String: Settings] {
do {
return try jsonDictionary.json(atKeyPath: "settingGroups", invalidItemBehaviour: .fail)
} catch let specParsingError as SpecParsingError {
// Re-throw `SpecParsingError` to prevent the misuse of settingGroups.
throw specParsingError
} catch {
// Ignore all errors except `SpecParsingError`
return jsonDictionary.json(atKeyPath: "settingPresets") ?? [:]
}
}
}
@@ -0,0 +1,58 @@
import Foundation
import JSONUtilities
/// Specifies the use of a plug-in product in a target.
public struct BuildToolPlugin: Equatable {
/// The name of the plug-in target.
public var plugin: String
/// The name of the package that defines the plug-in target.
public var package: String
public init(
plugin: String,
package: String
) {
self.plugin = plugin
self.package = package
}
}
extension BuildToolPlugin: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let plugin: String = jsonDictionary.json(atKeyPath: "plugin") {
self.plugin = plugin
} else {
throw SpecParsingError.invalidDependency(jsonDictionary)
}
if let package: String = jsonDictionary.json(atKeyPath: "package") {
self.package = package
} else {
throw SpecParsingError.invalidDependency(jsonDictionary)
}
}
}
extension BuildToolPlugin {
public var uniqueID: String {
return "\(plugin)/\(package)"
}
}
extension BuildToolPlugin: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(plugin)
hasher.combine(package)
}
}
extension BuildToolPlugin: JSONEncodable {
public func toJSONValue() -> Any {
[
"plugin": plugin,
"package": package
]
}
}
@@ -0,0 +1,33 @@
import Foundation
import XcodeGenCore
import Version
public class CacheFile {
public let string: String
init?(version: Version, projectDictionary: [String: Any], project: Project) throws {
guard #available(OSX 10.13, *) else { return nil }
let files = Set(project.allFiles)
.map { ((try? $0.relativePath(from: project.basePath)) ?? $0).string }
.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
.joined(separator: "\n")
let data = try JSONSerialization.data(withJSONObject: projectDictionary, options: [.sortedKeys, .prettyPrinted])
let spec = String(data: data, encoding: .utf8)!
string = """
# XCODEGEN VERSION
\(version)
# SPEC
\(spec)
# FILES
\(files)"
"""
}
}
+41
View File
@@ -0,0 +1,41 @@
import Foundation
import JSONUtilities
public struct Config: Hashable {
public var name: String
public var type: ConfigType?
public init(name: String, type: ConfigType? = nil) {
self.name = name
self.type = type
}
public static var defaultConfigs: [Config] = [Config(name: ConfigType.debug.name, type: .debug), Config(name: ConfigType.release.name, type: .release)]
}
public enum ConfigType: String, Hashable {
case debug
case release
public var name: String {
rawValue.prefix(1).uppercased() + rawValue.dropFirst()
}
}
extension Config {
public func matchesVariant(_ variant: String, for type: ConfigType) -> Bool {
guard self.type == type else { return false }
let nameWithoutType = self.name.lowercased()
.replacingOccurrences(of: type.name.lowercased(), with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: " -_()"))
return nameWithoutType == variant.lowercased()
}
}
public extension Collection where Element == Config {
func first(including configVariant: String, for type: ConfigType) -> Config? {
first { $0.matchesVariant(configVariant, for: type) }
}
}
+85
View File
@@ -0,0 +1,85 @@
import Foundation
import JSONUtilities
import PathKit
import Yams
extension Dictionary where Key: JSONKey {
public func json<T: NamedJSONDictionaryConvertible>(atKeyPath keyPath: JSONUtilities.KeyPath, invalidItemBehaviour: InvalidItemBehaviour<T> = .remove, parallel: Bool = false) throws -> [T] {
guard let dictionary = json(atKeyPath: keyPath) as JSONDictionary? else {
return []
}
if parallel {
let defaultError = NSError(domain: "Unspecified error", code: 0, userInfo: nil)
let keys = Array(dictionary.keys)
var itemResults: [Result<T, Error>] = Array(repeating: .failure(defaultError), count: keys.count)
itemResults.withUnsafeMutableBufferPointer { buffer in
let bufferWrapper = BufferWrapper(buffer: buffer)
DispatchQueue.concurrentPerform(iterations: dictionary.count) { idx in
do {
let key = keys[idx]
let jsonDictionary: JSONDictionary = try dictionary.json(atKeyPath: .key(key))
let item = try T(name: key, jsonDictionary: jsonDictionary)
bufferWrapper.buffer[idx] = .success(item)
} catch {
bufferWrapper.buffer[idx] = .failure(error)
}
}
}
return try itemResults.map { try $0.get() }
} else {
var items: [T] = []
for (key, _) in dictionary {
let jsonDictionary: JSONDictionary = try dictionary.json(atKeyPath: .key(key))
let item = try T(name: key, jsonDictionary: jsonDictionary)
items.append(item)
}
return items
}
}
public func json<T: NamedJSONConvertible>(atKeyPath keyPath: JSONUtilities.KeyPath, invalidItemBehaviour: InvalidItemBehaviour<T> = .remove) throws -> [T] {
guard let dictionary = json(atKeyPath: keyPath) as JSONDictionary? else {
return []
}
var items: [T] = []
for (key, value) in dictionary {
let item = try T(name: key, json: value)
items.append(item)
}
return items
}
}
private final class BufferWrapper<T>: @unchecked Sendable {
var buffer: UnsafeMutableBufferPointer<T>
init(buffer: UnsafeMutableBufferPointer<T>) {
self.buffer = buffer
}
}
public protocol NamedJSONDictionaryConvertible {
init(name: String, jsonDictionary: JSONDictionary) throws
}
public protocol NamedJSONConvertible {
init(name: String, json: Any) throws
}
extension JSONObjectConvertible {
public init(path: Path) throws {
let content: String = try path.read()
if content == "" {
try self.init(jsonDictionary: [:])
return
}
let yaml = try Yams.load(yaml: content)
guard let jsonDictionary = yaml as? JSONDictionary else {
throw JSONUtilsError.fileNotAJSONDictionary
}
try self.init(jsonDictionary: jsonDictionary)
}
}
@@ -0,0 +1,214 @@
import Foundation
import JSONUtilities
public struct Dependency: Equatable {
public static let removeHeadersDefault = true
public static let implicitDefault = false
public static let weakLinkDefault = false
public static let platformFilterDefault: PlatformFilter = .all
public var type: DependencyType
public var reference: String
public var embed: Bool?
public var codeSign: Bool?
public var removeHeaders: Bool = removeHeadersDefault
public var link: Bool?
public var implicit: Bool = implicitDefault
public var weakLink: Bool = weakLinkDefault
public var platformFilter: PlatformFilter = platformFilterDefault
public var destinationFilters: [SupportedDestination]?
public var platforms: Set<Platform>?
public var copyPhase: BuildPhaseSpec.CopyFilesSettings?
public init(
type: DependencyType,
reference: String,
embed: Bool? = nil,
codeSign: Bool? = nil,
link: Bool? = nil,
implicit: Bool = implicitDefault,
weakLink: Bool = weakLinkDefault,
platformFilter: PlatformFilter = platformFilterDefault,
destinationFilters: [SupportedDestination]? = nil,
platforms: Set<Platform>? = nil,
copyPhase: BuildPhaseSpec.CopyFilesSettings? = nil
) {
self.type = type
self.reference = reference
self.embed = embed
self.codeSign = codeSign
self.link = link
self.implicit = implicit
self.weakLink = weakLink
self.platformFilter = platformFilter
self.destinationFilters = destinationFilters
self.platforms = platforms
self.copyPhase = copyPhase
}
public enum PlatformFilter: String, Equatable {
case all
case iOS
case macOS
}
public enum CarthageLinkType: String {
case dynamic
case `static`
public static let `default` = dynamic
}
public enum DependencyType: Hashable {
case target
case framework
case carthage(findFrameworks: Bool?, linkType: CarthageLinkType)
case sdk(root: String?)
case package(products: [String])
case bundle
}
}
extension Dependency {
public var uniqueID: String {
switch type {
case .package(let products):
if !products.isEmpty {
return "\(reference)/\(products.joined(separator: ","))"
} else {
return reference
}
default: return reference
}
}
}
extension Dependency: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(reference)
hasher.combine(type)
}
}
extension Dependency: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let target: String = jsonDictionary.json(atKeyPath: "target") {
type = .target
reference = target
} else if let framework: String = jsonDictionary.json(atKeyPath: "framework") {
type = .framework
reference = framework
} else if let carthage: String = jsonDictionary.json(atKeyPath: "carthage") {
let findFrameworks: Bool? = jsonDictionary.json(atKeyPath: "findFrameworks")
let carthageLinkType: CarthageLinkType = (jsonDictionary.json(atKeyPath: "linkType") as String?).flatMap(CarthageLinkType.init(rawValue:)) ?? .default
type = .carthage(findFrameworks: findFrameworks, linkType: carthageLinkType)
reference = carthage
} else if let sdk: String = jsonDictionary.json(atKeyPath: "sdk") {
let sdkRoot: String? = jsonDictionary.json(atKeyPath: "root")
type = .sdk(root: sdkRoot)
reference = sdk
} else if let package: String = jsonDictionary.json(atKeyPath: "package") {
if let products: [String] = jsonDictionary.json(atKeyPath: "products") {
type = .package(products: products)
reference = package
} else if let product: String = jsonDictionary.json(atKeyPath: "product") {
type = .package(products: [product])
reference = package
} else {
type = .package(products: [])
reference = package
}
} else if let bundle: String = jsonDictionary.json(atKeyPath: "bundle") {
type = .bundle
reference = bundle
} else {
throw SpecParsingError.invalidDependency(jsonDictionary)
}
embed = jsonDictionary.json(atKeyPath: "embed")
codeSign = jsonDictionary.json(atKeyPath: "codeSign")
link = jsonDictionary.json(atKeyPath: "link")
if let bool: Bool = jsonDictionary.json(atKeyPath: "removeHeaders") {
removeHeaders = bool
}
if let bool: Bool = jsonDictionary.json(atKeyPath: "implicit") {
implicit = bool
}
if let bool: Bool = jsonDictionary.json(atKeyPath: "weak") {
weakLink = bool
}
if let platformFilterString: String = jsonDictionary.json(atKeyPath: "platformFilter"), let platformFilter = PlatformFilter(rawValue: platformFilterString) {
self.platformFilter = platformFilter
} else {
self.platformFilter = .all
}
if let destinationFilters: [SupportedDestination] = jsonDictionary.json(atKeyPath: "destinationFilters") {
self.destinationFilters = destinationFilters
}
if let platforms: [ProjectSpec.Platform] = jsonDictionary.json(atKeyPath: "platforms") {
self.platforms = Set(platforms)
}
if let object: JSONDictionary = jsonDictionary.json(atKeyPath: "copy") {
copyPhase = try BuildPhaseSpec.CopyFilesSettings(jsonDictionary: object)
}
}
}
extension Dependency: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"embed": embed,
"codeSign": codeSign,
"link": link,
"platforms": platforms?.map(\.rawValue).sorted(),
"copy": copyPhase?.toJSONValue(),
"destinationFilters": destinationFilters?.map { $0.rawValue },
]
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, let linkType):
dict["carthage"] = reference
if let findFrameworks = findFrameworks {
dict["findFrameworks"] = findFrameworks
}
dict["linkType"] = linkType.rawValue
case .sdk:
dict["sdk"] = reference
case .package:
dict["package"] = reference
case .bundle:
dict["bundle"] = reference
}
return dict
}
}
extension Dependency: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("framework"),
]
}
}
@@ -0,0 +1,103 @@
import Foundation
import JSONUtilities
import Version
public struct DeploymentTarget: Equatable {
public var iOS: Version?
public var tvOS: Version?
public var watchOS: Version?
public var macOS: Version?
public var visionOS: Version?
public init(
iOS: Version? = nil,
tvOS: Version? = nil,
watchOS: Version? = nil,
macOS: Version? = nil,
visionOS: Version? = nil
) {
self.iOS = iOS
self.tvOS = tvOS
self.watchOS = watchOS
self.macOS = macOS
self.visionOS = visionOS
}
public func version(for platform: Platform) -> Version? {
switch platform {
case .auto: return nil
case .iOS: return iOS
case .tvOS: return tvOS
case .watchOS: return watchOS
case .macOS: return macOS
case .visionOS: return visionOS
}
}
}
extension Platform {
public var deploymentTargetSetting: String {
switch self {
case .auto: return ""
case .iOS: return "IPHONEOS_DEPLOYMENT_TARGET"
case .tvOS: return "TVOS_DEPLOYMENT_TARGET"
case .watchOS: return "WATCHOS_DEPLOYMENT_TARGET"
case .macOS: return "MACOSX_DEPLOYMENT_TARGET"
case .visionOS: return "XROS_DEPLOYMENT_TARGET"
}
}
public var sdkRoot: String {
switch self {
case .auto: return "auto"
case .iOS: return "iphoneos"
case .tvOS: return "appletvos"
case .watchOS: return "watchos"
case .macOS: return "macosx"
case .visionOS: return "xros"
}
}
}
extension Version {
/// doesn't print patch if 0
public var deploymentTarget: String {
"\(major).\(minor)\(patch > 0 ? ".\(patch)" : "")"
}
}
extension DeploymentTarget: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
func parseVersion(_ platform: String) throws -> Version? {
if let string: String = jsonDictionary.json(atKeyPath: .key(platform)) {
return try Version.parse(string)
} else if let double: Double = jsonDictionary.json(atKeyPath: .key(platform)) {
return try Version.parse(double)
} else {
return nil
}
}
iOS = try parseVersion("iOS")
tvOS = try parseVersion("tvOS")
watchOS = try parseVersion("watchOS")
macOS = try parseVersion("macOS")
visionOS = try parseVersion("visionOS")
}
}
extension DeploymentTarget: JSONEncodable {
public func toJSONValue() -> Any {
[
"iOS": iOS?.description,
"tvOS": tvOS?.description,
"watchOS": watchOS?.description,
"macOS": macOS?.description,
"visionOS": visionOS?.description,
]
}
}
@@ -0,0 +1,33 @@
extension Dictionary where Key == String, Value == Any? {
func removingEmptyArraysDictionariesAndNils() -> [String: Any] {
var new: [String: Any] = [:]
filter(outNil).forEach { pair in
let value: Any
if let array = pair.value as? [[String: Any?]] {
value = array.removingEmptyArraysDictionariesAndNils()
} else if let dictionary = pair.value as? [String: Any?] {
value = dictionary.removingEmptyArraysDictionariesAndNils()
} else {
value = pair.value! // nil is filtered out :)
}
new[pair.key] = value
}
return new
.filter(outEmptyArrays)
.filter(outEmptyDictionaries)
}
func outEmptyArrays(_ pair: (key: String, value: Any)) -> Bool {
guard let array = pair.value as? [Any] else { return true }
return !array.isEmpty
}
func outEmptyDictionaries(_ pair: (key: String, value: Any)) -> Bool {
guard let dictionary = pair.value as? [String: Any] else { return true }
return !dictionary.isEmpty
}
func outNil(_ pair: (key: String, value: Any?)) -> Bool {
return pair.value != nil
}
}
@@ -0,0 +1,7 @@
import Foundation
import JSONUtilities
public protocol JSONEncodable {
// returns JSONDictionary or JSONArray or JSONRawType or nil
func toJSONValue() -> Any
}
+125
View File
@@ -0,0 +1,125 @@
//
// File.swift
//
//
// Created by Yonas Kolb on 1/5/20.
//
import Foundation
import JSONUtilities
import enum XcodeProj.BuildPhase
public struct FileType: Equatable {
public enum Defaults {
public static let file = true
}
public var file: Bool
public var buildPhase: BuildPhaseSpec?
public var attributes: [String]
public var resourceTags: [String]
public var compilerFlags: [String]
public init(
file: Bool = Defaults.file,
buildPhase: BuildPhaseSpec? = nil,
attributes: [String] = [],
resourceTags: [String] = [],
compilerFlags: [String] = []
) {
self.file = file
self.buildPhase = buildPhase
self.attributes = attributes
self.resourceTags = resourceTags
self.compilerFlags = compilerFlags
}
}
extension FileType: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") {
buildPhase = try BuildPhaseSpec(string: string)
} else if let dict: JSONDictionary = jsonDictionary.json(atKeyPath: "buildPhase") {
buildPhase = try BuildPhaseSpec(jsonDictionary: dict)
}
file = jsonDictionary.json(atKeyPath: "file") ?? Defaults.file
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? []
resourceTags = jsonDictionary.json(atKeyPath: "resourceTags") ?? []
compilerFlags = jsonDictionary.json(atKeyPath: "compilerFlags") ?? []
}
}
extension FileType: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"buildPhase": buildPhase?.toJSONValue(),
"attributes": attributes,
"resourceTags": resourceTags,
"compilerFlags": compilerFlags,
]
if file != Defaults.file {
dict["file"] = file
}
return dict
}
}
extension FileType {
public static let defaultFileTypes: [String: FileType] = [
// resources
"bundle": FileType(buildPhase: .resources),
"xcassets": FileType(buildPhase: .resources),
"storekit": FileType(buildPhase: .resources),
"xcstrings": FileType(buildPhase: .resources),
// sources
"swift": FileType(buildPhase: .sources),
"gyb": FileType(buildPhase: .sources),
"m": FileType(buildPhase: .sources),
"mm": FileType(buildPhase: .sources),
"cpp": FileType(buildPhase: .sources),
"cp": FileType(buildPhase: .sources),
"cxx": FileType(buildPhase: .sources),
"c": FileType(buildPhase: .sources),
"cc": FileType(buildPhase: .sources),
"S": FileType(buildPhase: .sources),
"xcdatamodeld": FileType(buildPhase: .sources),
"xcmappingmodel": FileType(buildPhase: .sources),
"intentdefinition": FileType(buildPhase: .sources),
"metal": FileType(buildPhase: .sources),
"mlmodel": FileType(buildPhase: .sources),
"mlpackage" : FileType(buildPhase: .sources),
"mlmodelc": FileType(buildPhase: .resources),
"rcproject": FileType(buildPhase: .sources),
"iig": FileType(buildPhase: .sources),
"docc": FileType(buildPhase: .sources),
// headers
"h": FileType(buildPhase: .headers),
"hh": FileType(buildPhase: .headers),
"hpp": FileType(buildPhase: .headers),
"ipp": FileType(buildPhase: .headers),
"tpp": FileType(buildPhase: .headers),
"hxx": FileType(buildPhase: .headers),
"def": FileType(buildPhase: .headers),
// frameworks
"framework": FileType(buildPhase: .frameworks),
// copyfiles
"xpc": FileType(buildPhase: .copyFiles(.xpcServices)),
"appex": FileType(buildPhase: .copyFiles(.plugins)),
// no build phase (not resources)
"xcconfig": FileType(buildPhase: BuildPhaseSpec.none),
"entitlements": FileType(buildPhase: BuildPhaseSpec.none),
"gpx": FileType(buildPhase: BuildPhaseSpec.none),
"lproj": FileType(buildPhase: BuildPhaseSpec.none),
"xcfilelist": FileType(buildPhase: BuildPhaseSpec.none),
"apns": FileType(buildPhase: BuildPhaseSpec.none),
"pch": FileType(buildPhase: BuildPhaseSpec.none),
"xctestplan": FileType(buildPhase: BuildPhaseSpec.none),
]
}
@@ -0,0 +1,32 @@
import Foundation
import JSONUtilities
/// Describes an order of groups.
public struct GroupOrdering: Equatable {
/// A group name pattern.
public var pattern: String
/// A group name regex.
public var regex: NSRegularExpression?
/// Subgroups orders.
public var order: [String]
public init(pattern: String = "", order: [String] = []) {
self.pattern = pattern
self.regex = try? NSRegularExpression(pattern: pattern)
self.order = order
}
}
extension GroupOrdering: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
pattern = jsonDictionary.json(atKeyPath: "pattern") ?? ""
regex = try? NSRegularExpression(pattern: pattern)
order = jsonDictionary.json(atKeyPath: "order") ?? []
}
}
+61
View File
@@ -0,0 +1,61 @@
import Foundation
import XcodeProj
public enum Linkage {
case dynamic
case `static`
case none
}
extension Target {
public var defaultLinkage: Linkage {
switch type {
case .none,
.appExtension,
.application,
.bundle,
.commandLineTool,
.instrumentsPackage,
.intentsServiceExtension,
.messagesApplication,
.messagesExtension,
.metalLibrary,
.ocUnitTestBundle,
.onDemandInstallCapableApplication,
.stickerPack,
.tvExtension,
.uiTestBundle,
.unitTestBundle,
.watchApp,
.watchExtension,
.watch2App,
.watch2AppContainer,
.watch2Extension,
.xcodeExtension,
.xpcService,
.systemExtension,
.driverExtension,
.extensionKitExtension:
return .none
case .framework, .xcFramework:
// Check the MACH_O_TYPE for "Static Framework"
if settings.buildSettings.machOType == "staticlib" {
return .static
} else {
return .dynamic
}
case .dynamicLibrary:
return .dynamic
case .staticLibrary, .staticFramework:
return .static
}
}
}
private extension BuildSettings {
var machOType: String? {
self["MACH_O_TYPE"] as? String
}
}
@@ -0,0 +1,10 @@
import Foundation
public extension NSRegularExpression {
func isMatch(to string: String) -> Bool {
let range = NSRange(location: 0, length: string.utf16.count)
return self.firstMatch(in: string, options: [], range: range) != nil
}
}
@@ -0,0 +1,62 @@
import Foundation
import JSONUtilities
import PathKit
protocol PathContainer {
static var pathProperties: [PathProperty] { get }
}
enum PathProperty {
case string(String)
case dictionary([PathProperty])
case object(String, [PathProperty])
}
extension Array where Element == PathProperty {
func resolvingPaths(in jsonDictionary: JSONDictionary, relativeTo path: Path) -> JSONDictionary {
var result = jsonDictionary
for pathProperty in self {
switch pathProperty {
case .string(let key):
if let source = result[key] as? String {
result[key] = (path + source).string
} else if let source = result[key] as? [Any] {
result[key] = source.map { any -> Any in
if let string = any as? String {
return (path + string).string
} else {
return any
}
}
} else if let source = result[key] as? [String: String] {
result[key] = source.mapValues { (path + $0).string }
}
case .dictionary(let pathProperties):
for (key, dictionary) in result {
if let source = dictionary as? JSONDictionary {
result[key] = pathProperties.resolvingPaths(in: source, relativeTo: path)
}
}
case .object(let key, let pathProperties):
if let source = result[key] as? JSONDictionary {
result[key] = pathProperties.resolvingPaths(in: source, relativeTo: path)
} else if let source = result[key] as? [Any] {
result[key] = source.map { any -> Any in
if let dictionary = any as? JSONDictionary {
return pathProperties.resolvingPaths(in: dictionary, relativeTo: path)
} else {
return any
}
}
} else if let source = result[key] as? [String: JSONDictionary] {
result[key] = source.mapValues { pathProperties.resolvingPaths(in: $0, relativeTo: path) }
}
}
}
return result
}
}
+10
View File
@@ -0,0 +1,10 @@
import Foundation
public enum Platform: String, Hashable, CaseIterable {
case auto
case iOS
case tvOS
case macOS
case watchOS
case visionOS
}
+44
View File
@@ -0,0 +1,44 @@
import Foundation
import JSONUtilities
public struct Plist: Equatable {
public let path: String
public let properties: [String: Any]
public init(path: String, attributes: [String: Any] = [:]) {
self.path = path
properties = attributes
}
public static func == (lhs: Plist, rhs: Plist) -> Bool {
lhs.path == rhs.path &&
NSDictionary(dictionary: lhs.properties).isEqual(to: rhs.properties)
}
}
extension Plist: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
path = try jsonDictionary.json(atKeyPath: "path")
properties = jsonDictionary.json(atKeyPath: "properties") ?? [:]
}
}
extension Plist: JSONEncodable {
public func toJSONValue() -> Any {
[
"path": path,
"properties": properties,
] as [String : Any]
}
}
extension Plist: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("path"),
]
}
}
+319
View File
@@ -0,0 +1,319 @@
import Foundation
import JSONUtilities
import PathKit
import Yams
public struct Project: BuildSettingsContainer {
public var basePath: Path
public var name: String
public var targets: [Target] {
didSet {
targetsMap = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })
}
}
public var aggregateTargets: [AggregateTarget] {
didSet {
aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) })
}
}
public var packages: [String: SwiftPackage]
public var settings: Settings
public var settingGroups: [String: Settings]
public var configs: [Config]
public var schemes: [Scheme]
public var breakpoints: [Breakpoint]
public var options: SpecOptions
public var attributes: [String: Any]
public var fileGroups: [String]
public var configFiles: [String: String]
public var include: [String] = []
public var projectReferences: [ProjectReference] = [] {
didSet {
projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
}
}
private var targetsMap: [String: Target]
private var aggregateTargetsMap: [String: AggregateTarget]
private var projectReferencesMap: [String: ProjectReference]
public init(
basePath: Path = "",
name: String,
configs: [Config] = Config.defaultConfigs,
targets: [Target] = [],
aggregateTargets: [AggregateTarget] = [],
settings: Settings = .empty,
settingGroups: [String: Settings] = [:],
schemes: [Scheme] = [],
breakpoints: [Breakpoint] = [],
packages: [String: SwiftPackage] = [:],
options: SpecOptions = SpecOptions(),
fileGroups: [String] = [],
configFiles: [String: String] = [:],
attributes: [String: Any] = [:],
projectReferences: [ProjectReference] = []
) {
self.basePath = basePath
self.name = name
self.targets = targets
targetsMap = Dictionary(uniqueKeysWithValues: self.targets.map { ($0.name, $0) })
self.aggregateTargets = aggregateTargets
aggregateTargetsMap = Dictionary(uniqueKeysWithValues: self.aggregateTargets.map { ($0.name, $0) })
self.configs = configs
self.settings = settings
self.settingGroups = settingGroups
self.schemes = schemes
self.breakpoints = breakpoints
self.packages = packages
self.options = options
self.fileGroups = fileGroups
self.configFiles = configFiles
self.attributes = attributes
self.projectReferences = projectReferences
projectReferencesMap = Dictionary(uniqueKeysWithValues: self.projectReferences.map { ($0.name, $0) })
}
public func getProjectReference(_ projectName: String) -> ProjectReference? {
projectReferencesMap[projectName]
}
public func getTarget(_ targetName: String) -> Target? {
targetsMap[targetName]
}
public func getPackage(_ packageName: String) -> SwiftPackage? {
packages[packageName]
}
public func getAggregateTarget(_ targetName: String) -> AggregateTarget? {
aggregateTargetsMap[targetName]
}
public func getProjectTarget(_ targetName: String) -> ProjectTarget? {
targetsMap[targetName] ?? aggregateTargetsMap[targetName]
}
public func getConfig(_ configName: String) -> Config? {
configs.first { $0.name == configName }
}
public var defaultProjectPath: Path {
basePath + "\(name).xcodeproj"
}
}
extension Project: CustomDebugStringConvertible {
public var debugDescription: String {
var string = "Name: \(name)"
let indent = " "
if !include.isEmpty {
string += "\nInclude:\n\(indent)" + include.map { $0.description }.joined(separator: "\n\(indent)")
}
if !settingGroups.isEmpty {
string += "\nSetting Groups:\n\(indent)" + settingGroups.keys
.sorted()
.joined(separator: "\n\(indent)")
}
if !targets.isEmpty {
string += "\nTargets:\n\(indent)" + targets.map { $0.description }.joined(separator: "\n\(indent)")
}
if !aggregateTargets.isEmpty {
string += "\nAggregate Targets:\n\(indent)" + aggregateTargets.map { $0.description }.joined(separator: "\n\(indent)")
}
if !schemes.isEmpty {
let allSchemes = targets.filter { $0.scheme != nil }.map { $0.name } + schemes.map { $0.name }
string += "\nSchemes:\n\(indent)" + allSchemes.joined(separator: "\n\(indent)")
}
return string
}
}
extension Project: Equatable {
public static func == (lhs: Project, rhs: Project) -> Bool {
lhs.name == rhs.name &&
lhs.targets == rhs.targets &&
lhs.aggregateTargets == rhs.aggregateTargets &&
lhs.settings == rhs.settings &&
lhs.settingGroups == rhs.settingGroups &&
lhs.configs == rhs.configs &&
lhs.schemes == rhs.schemes &&
lhs.breakpoints == rhs.breakpoints &&
lhs.fileGroups == rhs.fileGroups &&
lhs.configFiles == rhs.configFiles &&
lhs.options == rhs.options &&
lhs.packages == rhs.packages &&
NSDictionary(dictionary: lhs.attributes).isEqual(to: rhs.attributes)
}
}
extension Project {
public init(path: Path) throws {
let spec = try SpecFile(path: path)
try self.init(spec: spec)
}
public init(spec: SpecFile) throws {
try self.init(basePath: spec.basePath, jsonDictionary: spec.resolvedDictionary())
}
public init(basePath: Path = "", jsonDictionary: JSONDictionary) throws {
self.basePath = basePath
let jsonDictionary = Project.resolveProject(jsonDictionary: jsonDictionary)
let buildSettingsParser = BuildSettingsParser(jsonDictionary: jsonDictionary)
name = try jsonDictionary.json(atKeyPath: "name")
settings = try buildSettingsParser.parse()
settingGroups = try buildSettingsParser.parseSettingGroups()
let configs: [String: String] = jsonDictionary.json(atKeyPath: "configs") ?? [:]
self.configs = configs.isEmpty ? Config.defaultConfigs :
configs.map { Config(name: $0, type: ConfigType(rawValue: $1)) }.sorted { $0.name < $1.name }
targets = try jsonDictionary.json(atKeyPath: "targets", parallel: true).sorted { $0.name < $1.name }
aggregateTargets = try jsonDictionary.json(atKeyPath: "aggregateTargets").sorted { $0.name < $1.name }
projectReferences = try jsonDictionary.json(atKeyPath: "projectReferences").sorted { $0.name < $1.name }
schemes = try jsonDictionary.json(atKeyPath: "schemes")
if jsonDictionary["breakpoints"] != nil {
breakpoints = try jsonDictionary.json(atKeyPath: "breakpoints", invalidItemBehaviour: .fail)
} else {
breakpoints = []
}
fileGroups = jsonDictionary.json(atKeyPath: "fileGroups") ?? []
configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:]
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
include = jsonDictionary.json(atKeyPath: "include") ?? []
if jsonDictionary["packages"] != nil {
packages = try jsonDictionary.json(atKeyPath: "packages", invalidItemBehaviour: .fail)
} else {
packages = [:]
}
// For backward compatibility of old `localPackages:` format
if let localPackages: [String] = jsonDictionary.json(atKeyPath: "localPackages") {
packages.merge(localPackages.reduce(into: [String: SwiftPackage]()) {
// Project name will be obtained by resolved abstractpath's lastComponent for dealing with some path case, like "../"
let packageName = (basePath + Path($1).normalize()).lastComponent
$0[packageName] = .local(path: $1, group: nil, excludeFromProject: false)
}
)
}
if jsonDictionary["options"] != nil {
options = try jsonDictionary.json(atKeyPath: "options")
} else {
options = SpecOptions()
}
targetsMap = Dictionary(uniqueKeysWithValues: targets.map { ($0.name, $0) })
aggregateTargetsMap = Dictionary(uniqueKeysWithValues: aggregateTargets.map { ($0.name, $0) })
projectReferencesMap = Dictionary(uniqueKeysWithValues: projectReferences.map { ($0.name, $0) })
}
static func resolveProject(jsonDictionary: JSONDictionary) -> JSONDictionary {
var jsonDictionary = jsonDictionary
// resolve multiple times so that we support both multi-platform templates,
// as well as platform specific templates in multi-platform targets
jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary)
jsonDictionary = Target.resolveTargetTemplates(jsonDictionary: jsonDictionary)
jsonDictionary = Scheme.resolveSchemeTemplates(jsonDictionary: jsonDictionary)
jsonDictionary = Target.resolveMultiplatformTargets(jsonDictionary: jsonDictionary)
return jsonDictionary
}
}
extension Project: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("configFiles"),
.object("options", SpecOptions.pathProperties),
.object("targets", Target.pathProperties),
.object("targetTemplates", Target.pathProperties),
.object("aggregateTargets", AggregateTarget.pathProperties),
.object("schemes", Scheme.pathProperties),
.object("projectReferences", ProjectReference.pathProperties),
.object("packages", SwiftPackage.pathProperties),
.string("localPackages"),
.string("fileGroups")
]
}
}
extension Project {
public var allFiles: [Path] {
var files: [Path] = []
files.append(contentsOf: configFilePaths)
for fileGroup in fileGroups {
let fileGroupPath = basePath + fileGroup
let fileGroupChildren = (try? fileGroupPath.recursiveChildren()) ?? []
files.append(contentsOf: fileGroupChildren)
files.append(fileGroupPath)
}
for target in aggregateTargets {
files.append(contentsOf: target.configFilePaths)
}
for target in targets {
files.append(contentsOf: target.configFilePaths)
for source in target.sources {
let sourcePath = basePath + source.path
let sourceChildren = (try? sourcePath.recursiveChildren()) ?? []
files.append(contentsOf: sourceChildren)
files.append(sourcePath)
}
}
return files
}
}
extension BuildSettingsContainer {
fileprivate var configFilePaths: [Path] {
configFiles.values.map { Path($0) }
}
}
extension Project: JSONEncodable {
public func toJSONValue() -> Any {
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()) }
let projectReferencesPairs = projectReferences.map { ($0.name, $0.toJSONValue()) }
var dictionary: JSONDictionary = [:]
dictionary["name"] = name
dictionary["options"] = options.toJSONValue()
dictionary["settings"] = settings.toJSONValue()
dictionary["fileGroups"] = fileGroups
dictionary["configFiles"] = configFiles
dictionary["include"] = include
dictionary["attributes"] = attributes
dictionary["packages"] = packages.mapValues { $0.toJSONValue() }
dictionary["targets"] = Dictionary(uniqueKeysWithValues: targetPairs)
dictionary["configs"] = Dictionary(uniqueKeysWithValues: configsPairs)
dictionary["aggregateTargets"] = Dictionary(uniqueKeysWithValues: aggregateTargetsPairs)
dictionary["schemes"] = Dictionary(uniqueKeysWithValues: schemesPairs)
dictionary["settingGroups"] = settingGroups.mapValues { $0.toJSONValue() }
dictionary["projectReferences"] = Dictionary(uniqueKeysWithValues: projectReferencesPairs)
return dictionary
}
}
@@ -0,0 +1,38 @@
import Foundation
import JSONUtilities
public struct ProjectReference: Hashable {
public var name: String
public var path: String
public init(name: String, path: String) {
self.name = name
self.path = path
}
}
extension ProjectReference: PathContainer {
static var pathProperties: [PathProperty] {
[
.dictionary([
.string("path"),
]),
]
}
}
extension ProjectReference: NamedJSONDictionaryConvertible {
public init(name: String, jsonDictionary: JSONDictionary) throws {
self.name = name
self.path = try jsonDictionary.json(atKeyPath: "path")
}
}
extension ProjectReference: JSONEncodable {
public func toJSONValue() -> Any {
[
"path": path,
]
}
}
@@ -0,0 +1,26 @@
import Foundation
import XcodeProj
public protocol ProjectTarget: BuildSettingsContainer {
var name: String { get }
var type: PBXProductType { get }
var buildScripts: [BuildScript] { get }
var buildToolPlugins: [BuildToolPlugin] { get }
var scheme: TargetScheme? { get }
var attributes: [String: Any] { get }
}
extension Target {
public var buildScripts: [BuildScript] {
preBuildScripts + postCompileScripts + postBuildScripts
}
}
extension Project {
public var projectTargets: [ProjectTarget] {
targets.map { $0 as ProjectTarget } + aggregateTargets.map { $0 as ProjectTarget }
}
}
File diff suppressed because it is too large Load Diff
+141
View File
@@ -0,0 +1,141 @@
import Foundation
import JSONUtilities
import PathKit
import XcodeProj
public struct Settings: Equatable, JSONObjectConvertible, CustomStringConvertible {
public var buildSettings: BuildSettings
public var configSettings: [String: Settings]
public var groups: [String]
public init(buildSettings: BuildSettings = [:], configSettings: [String: Settings] = [:], groups: [String] = []) {
self.buildSettings = buildSettings
self.configSettings = configSettings
self.groups = groups
}
public init(dictionary: [String: Any]) {
buildSettings = dictionary
configSettings = [:]
groups = []
}
public static let empty: Settings = Settings(dictionary: [:])
public init(jsonDictionary: JSONDictionary) throws {
if jsonDictionary["configs"] != nil || jsonDictionary["groups"] != nil || jsonDictionary["base"] != nil {
groups = jsonDictionary.json(atKeyPath: "groups") ?? jsonDictionary.json(atKeyPath: "presets") ?? []
let buildSettingsDictionary: JSONDictionary = jsonDictionary.json(atKeyPath: "base") ?? [:]
buildSettings = buildSettingsDictionary
self.configSettings = try Self.extractValidConfigs(from: jsonDictionary)
} else {
buildSettings = jsonDictionary
configSettings = [:]
groups = []
}
}
/// Extracts and validates the `configs` mapping from the given JSON dictionary.
/// - Parameter jsonDictionary: The JSON dictionary to extract `configs` from.
/// - Returns: A dictionary mapping configuration names to `Settings` objects.
private static func extractValidConfigs(from jsonDictionary: JSONDictionary) throws -> [String: Settings] {
guard let configSettings = jsonDictionary["configs"] as? JSONDictionary else {
return [:]
}
let invalidConfigKeys = Set(
configSettings.filter { !($0.value is JSONDictionary) }
.map(\.key)
)
guard invalidConfigKeys.isEmpty else {
throw SpecParsingError.invalidConfigsMappingFormat(keys: invalidConfigKeys)
}
return try jsonDictionary.json(atKeyPath: "configs")
}
public static func == (lhs: Settings, rhs: Settings) -> Bool {
NSDictionary(dictionary: lhs.buildSettings).isEqual(to: rhs.buildSettings) &&
lhs.configSettings == rhs.configSettings &&
lhs.groups == rhs.groups
}
public var description: String {
var string: String = ""
if !buildSettings.isEmpty {
let buildSettingDescription = buildSettings.map { "\($0) = \($1)" }.joined(separator: "\n")
if !configSettings.isEmpty || !groups.isEmpty {
string += "base:\n " + buildSettingDescription.replacingOccurrences(of: "(.)\n", with: "$1\n ", options: .regularExpression, range: nil)
} else {
string += buildSettingDescription
}
}
if !configSettings.isEmpty {
if !string.isEmpty {
string += "\n"
}
for (config, buildSettings) in configSettings {
if !buildSettings.description.isEmpty {
string += "configs:\n"
string += " \(config):\n " + buildSettings.description.replacingOccurrences(of: "(.)\n", with: "$1\n ", options: .regularExpression, range: nil)
}
}
}
if !groups.isEmpty {
if !string.isEmpty {
string += "\n"
}
string += "groups:\n \(groups.joined(separator: "\n "))"
}
return string
}
}
extension Settings: ExpressibleByDictionaryLiteral {
public init(dictionaryLiteral elements: (String, Any)...) {
var dictionary: [String: Any] = [:]
elements.forEach { dictionary[$0.0] = $0.1 }
self.init(dictionary: dictionary)
}
}
extension Dictionary where Key == String, Value: Any {
public func merged(_ dictionary: [Key: Value]) -> [Key: Value] {
var mergedDictionary = self
mergedDictionary.merge(dictionary)
return mergedDictionary
}
public mutating func merge(_ dictionary: [Key: Value]) {
for (key, value) in dictionary {
self[key] = value
}
}
public func equals(_ dictionary: BuildSettings) -> Bool {
NSDictionary(dictionary: self).isEqual(to: dictionary)
}
}
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() },
] as [String : Any]
}
return buildSettings
}
}
@@ -0,0 +1,14 @@
//
// File.swift
//
//
// Created by Yonas Kolb on 1/5/20.
//
import Foundation
public enum SourceType: String {
case group
case file
case folder
}
+261
View File
@@ -0,0 +1,261 @@
import Foundation
import JSONUtilities
import PathKit
import Yams
public struct SpecFile {
/// For the root spec, this is the folder containing the SpecFile. For subSpecs this is the path
/// to the folder of the parent spec that is including this SpecFile.
public let basePath: Path
public let jsonDictionary: JSONDictionary
public let subSpecs: [SpecFile]
/// The relative path to use when resolving paths in the json dictionary. Is an empty path when
/// included with relativePaths disabled.
private let relativePath: Path
/// The path to the file relative to the basePath.
private let filePath: Path
fileprivate struct Include {
let path: Path
let relativePaths: Bool
let enable: Bool
static let defaultRelativePaths = true
static let defaultEnable = true
init?(any: Any) {
if let string = any as? String {
path = Path(string)
relativePaths = Include.defaultRelativePaths
enable = Include.defaultEnable
} else if let dictionary = any as? JSONDictionary, let path = dictionary["path"] as? String {
self.path = Path(path)
relativePaths = Self.resolveBoolean(dictionary, key: "relativePaths") ?? Include.defaultRelativePaths
enable = Self.resolveBoolean(dictionary, key: "enable") ?? Include.defaultEnable
} else {
return nil
}
}
static func parse(json: Any?) -> [Include] {
if let array = json as? [Any] {
return array.compactMap(Include.init)
} else if let object = json, let include = Include(any: object) {
return [include]
} else {
return []
}
}
private static func resolveBoolean(_ dictionary: [String: Any], key: String) -> Bool? {
dictionary[key] as? Bool ?? (dictionary[key] as? NSString)?.boolValue
}
}
/// Create a SpecFile for a Project
/// - Parameters:
/// - path: The absolute path to the spec file
/// - projectRoot: The root of the project to use as the base path. When nil, uses the parent
/// of the path.
public init(path: Path, projectRoot: Path? = nil, variables: [String: String] = [:]) throws {
let basePath = projectRoot ?? path.parent()
let filePath = try path.relativePath(from: basePath)
var cachedSpecFiles: [Path: SpecFile] = [:]
try self.init(filePath: filePath, basePath: basePath, cachedSpecFiles: &cachedSpecFiles, variables: variables)
}
/// Memberwise initializer for SpecFile
public init(filePath: Path, jsonDictionary: JSONDictionary, basePath: Path = "", relativePath: Path = "", subSpecs: [SpecFile] = []) {
self.basePath = basePath
self.relativePath = relativePath
self.jsonDictionary = jsonDictionary
self.subSpecs = subSpecs
self.filePath = filePath
}
private init(include: Include, basePath: Path, relativePath: Path, cachedSpecFiles: inout [Path: SpecFile], variables: [String: String]) throws {
let basePath = include.relativePaths ? (basePath + relativePath) : basePath
let relativePath = include.relativePaths ? include.path.parent() : Path()
try self.init(filePath: include.path, basePath: basePath, cachedSpecFiles: &cachedSpecFiles, variables: variables, relativePath: relativePath)
}
private init(filePath: Path, basePath: Path, cachedSpecFiles: inout [Path: SpecFile], variables: [String: String], relativePath: Path = "") throws {
let path = basePath + filePath
if let specFile = cachedSpecFiles[path] {
self = specFile
return
}
let jsonDictionary = try SpecFile.loadDictionary(path: path).expand(variables: variables)
let includes = Include.parse(json: jsonDictionary["include"])
let subSpecs: [SpecFile] = try includes
.filter(\.enable)
.map { include in
return try SpecFile(include: include, basePath: basePath, relativePath: relativePath, cachedSpecFiles: &cachedSpecFiles, variables: variables)
}
self.init(filePath: filePath, jsonDictionary: jsonDictionary, basePath: basePath, relativePath: relativePath, subSpecs: subSpecs)
cachedSpecFiles[path] = self
}
static func loadDictionary(path: Path) throws -> JSONDictionary {
// Depending on the extension we will either load the file as YAML or JSON
if path.extension?.lowercased() == "json" {
let data: Data = try path.read()
let jsonData = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
guard let jsonDictionary = jsonData as? [String: Any] else {
fatalError("Invalid JSON at path \(path)")
}
return jsonDictionary
} else {
return try loadYamlDictionary(path: path)
}
}
public func resolvedDictionary() -> JSONDictionary {
resolvedDictionaryWithUniqueTargets()
}
private func resolvedDictionaryWithUniqueTargets() -> JSONDictionary {
var cachedSpecFiles: [Path: SpecFile] = [:]
let resolvedSpec = resolvingPaths(cachedSpecFiles: &cachedSpecFiles)
var mergedSpecPaths = Set<Path>()
return resolvedSpec.mergedDictionary(set: &mergedSpecPaths)
}
private func mergedDictionary(set mergedSpecPaths: inout Set<Path>) -> JSONDictionary {
let path = basePath + filePath
guard mergedSpecPaths.insert(path).inserted else { return [:] }
return jsonDictionary.merged(onto:
subSpecs
.map { $0.mergedDictionary(set: &mergedSpecPaths) }
.reduce([:]) { $1.merged(onto: $0) })
}
private func resolvingPaths(cachedSpecFiles: inout [Path: SpecFile], relativeTo basePath: Path = Path()) -> SpecFile {
let path = basePath + filePath
if let cachedSpecFile = cachedSpecFiles[path] {
return cachedSpecFile
}
let relativePath = (basePath + self.relativePath).normalize()
guard relativePath != Path() else {
return self
}
let jsonDictionary = Project.pathProperties.resolvingPaths(in: self.jsonDictionary, relativeTo: relativePath)
let specFile = SpecFile(
filePath: filePath,
jsonDictionary: jsonDictionary,
basePath: self.basePath,
relativePath: self.relativePath,
subSpecs: subSpecs.map { $0.resolvingPaths(cachedSpecFiles: &cachedSpecFiles, relativeTo: relativePath) }
)
cachedSpecFiles[path] = specFile
return specFile
}
}
extension Dictionary where Key == String, Value: Any {
func merged(onto other: [Key: Value]) -> [Key: Value] {
var merged = other
for (key, value) in self {
if key.hasSuffix(":REPLACE") {
let newKey = key[key.startIndex..<key.index(key.endIndex, offsetBy: -8)]
merged[Key(newKey)] = value
} else if let dictionary = value as? [Key: Value], let base = merged[key] as? [Key: Value] {
merged[key] = dictionary.merged(onto: base) as? Value
} else if let array = value as? [Any], let base = merged[key] as? [Any] {
merged[key] = (base + array) as? Value
} else {
merged[key] = value
}
}
return merged
}
func expand(variables: [String: String]) -> JSONDictionary {
var expanded: JSONDictionary = self
if !variables.isEmpty {
for (key, value) in self {
let newKey = expand(variables: variables, in: key)
if newKey != key {
expanded.removeValue(forKey: key)
}
expanded[newKey] = expand(variables: variables, in: value)
}
}
return expanded
}
private func expand(variables: [String: String], in value: Any) -> Any {
switch value {
case let dictionary as JSONDictionary:
return dictionary.expand(variables: variables)
case let string as String:
return expand(variables: variables, in: string)
case let array as [JSONDictionary]:
return array.map { $0.expand(variables: variables) }
case let array as [String]:
return array.map { self.expand(variables: variables, in: $0) }
case let anyArray as [Any]:
return anyArray.map { self.expand(variables: variables, in: $0) }
default:
return value
}
}
private func expand(variables: [String: String], in string: String) -> String {
var result = string
var index = result.startIndex
while index < result.endIndex {
let substring = result[index...]
if substring.count < 4 {
// We need at least 4 characters: ${x}
index = result.endIndex
} else if substring[index] == "$"
&& substring[substring.index(index, offsetBy: 1)] == "{"
&& substring[substring.index(index, offsetBy: 2)] != "}" {
// This is the start of a variable expansion...
let variableStart = index
if let variableEnd = substring.firstIndex(of: "}") {
// ...with an end
let nameStart = result.index(variableStart, offsetBy: 2) // Skipping ${
let nameEnd = result.index(variableEnd, offsetBy: -1) // Removing trailing }
let name = result[nameStart...nameEnd]
if let value = variables[String(name)] {
result.replaceSubrange(variableStart...variableEnd, with: value)
index = result.index(index, offsetBy: value.count)
} else {
// Skip this whole variable for which we don't have a value
index = result.index(after: variableEnd)
}
} else {
// Malformed variable, skip the whole string
index = result.endIndex
}
} else {
// Move on to the next $ and start again or finish early
index = result[result.index(after: index)...].firstIndex(of: "$") ?? result.endIndex
}
}
return result
}
}
@@ -0,0 +1,73 @@
import Foundation
import JSONUtilities
import PathKit
import XcodeProj
import Yams
import Version
public class SpecLoader {
var project: Project!
public private(set) var projectDictionary: [String: Any]?
let version: Version
public init(version: Version) {
self.version = version
}
public func loadProject(path: Path, projectRoot: Path? = nil, variables: [String: String] = [:]) throws -> Project {
let projectRoot = projectRoot?.absolute()
let spec = try SpecFile(path: path, projectRoot: projectRoot, variables: variables)
let resolvedDictionary = spec.resolvedDictionary()
let project = try Project(basePath: projectRoot ?? spec.basePath, jsonDictionary: resolvedDictionary)
self.project = project
projectDictionary = resolvedDictionary
return project
}
public func validateProjectDictionaryWarnings() throws {
try projectDictionary?.validateWarnings()
}
public func generateCacheFile() throws -> CacheFile? {
guard let projectDictionary = projectDictionary,
let project = project else {
return nil
}
return try CacheFile(
version: version,
projectDictionary: projectDictionary,
project: project
)
}
}
private extension Dictionary where Key == String, Value: Any {
func validateWarnings() throws {
let errors: [SpecValidationError.ValidationError] = []
if !errors.isEmpty {
throw SpecValidationError(errors: errors)
}
}
func hasValueContaining(_ needle: String) -> Bool {
values.contains { value in
switch value {
case let dictionary as JSONDictionary:
return dictionary.hasValueContaining(needle)
case let string as String:
return string.contains(needle)
case let array as [JSONDictionary]:
return array.contains { $0.hasValueContaining(needle) }
case let array as [String]:
return array.contains { $0.contains(needle) }
default:
return false
}
}
}
}
@@ -0,0 +1,224 @@
import Foundation
import JSONUtilities
import Version
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 static let useBaseInternationalizationDefault = true
public static let schemePathPrefixDefault = "../../"
public var minimumXcodeGenVersion: Version?
public var carthageBuildPath: String?
public var carthageExecutablePath: String?
public var createIntermediateGroups: Bool
public var bundleIdPrefix: String?
public var settingPresets: SettingPresets
public var disabledValidations: [ValidationType]
public var developmentLanguage: String?
public var usesTabs: Bool?
public var tabWidth: UInt?
public var indentWidth: UInt?
public var xcodeVersion: String?
public var deploymentTarget: DeploymentTarget
public var defaultConfig: String?
public var transitivelyLinkDependencies: Bool
public var groupSortPosition: GroupSortPosition
public var groupOrdering: [GroupOrdering]
public var fileTypes: [String: FileType]
public var generateEmptyDirectories: Bool
public var findCarthageFrameworks: Bool
public var localPackagesGroup: String?
public var preGenCommand: String?
public var postGenCommand: String?
public var useBaseInternationalization: Bool
public var schemePathPrefix: String
public enum ValidationType: String {
case missingConfigs
case missingConfigFiles
case missingTestPlans
}
public enum SettingPresets: String {
case all
case none
case project
case targets
public var applyTarget: Bool {
switch self {
case .all, .targets: return true
default: return false
}
}
public var applyProject: Bool {
switch self {
case .all, .project: return true
default: return false
}
}
}
/// Where groups are sorted in relation to other files
public enum GroupSortPosition: String {
/// groups are at the top
case top
/// groups are at the bottom
case bottom
/// groups are sorted with the rest of the files
case none
}
public init(
minimumXcodeGenVersion: Version? = nil,
carthageBuildPath: String? = nil,
carthageExecutablePath: String? = nil,
createIntermediateGroups: Bool = createIntermediateGroupsDefault,
bundleIdPrefix: String? = nil,
settingPresets: SettingPresets = settingPresetsDefault,
developmentLanguage: String? = nil,
indentWidth: UInt? = nil,
tabWidth: UInt? = nil,
usesTabs: Bool? = nil,
xcodeVersion: String? = nil,
deploymentTarget: DeploymentTarget = .init(),
disabledValidations: [ValidationType] = [],
defaultConfig: String? = nil,
transitivelyLinkDependencies: Bool = transitivelyLinkDependenciesDefault,
groupSortPosition: GroupSortPosition = groupSortPositionDefault,
groupOrdering: [GroupOrdering] = [],
fileTypes: [String: FileType] = [:],
generateEmptyDirectories: Bool = generateEmptyDirectoriesDefault,
findCarthageFrameworks: Bool = findCarthageFrameworksDefault,
localPackagesGroup: String? = nil,
preGenCommand: String? = nil,
postGenCommand: String? = nil,
useBaseInternationalization: Bool = useBaseInternationalizationDefault,
schemePathPrefix: String = schemePathPrefixDefault
) {
self.minimumXcodeGenVersion = minimumXcodeGenVersion
self.carthageBuildPath = carthageBuildPath
self.carthageExecutablePath = carthageExecutablePath
self.createIntermediateGroups = createIntermediateGroups
self.bundleIdPrefix = bundleIdPrefix
self.settingPresets = settingPresets
self.developmentLanguage = developmentLanguage
self.tabWidth = tabWidth
self.indentWidth = indentWidth
self.usesTabs = usesTabs
self.xcodeVersion = xcodeVersion
self.deploymentTarget = deploymentTarget
self.disabledValidations = disabledValidations
self.defaultConfig = defaultConfig
self.transitivelyLinkDependencies = transitivelyLinkDependencies
self.groupSortPosition = groupSortPosition
self.groupOrdering = groupOrdering
self.fileTypes = fileTypes
self.generateEmptyDirectories = generateEmptyDirectories
self.findCarthageFrameworks = findCarthageFrameworks
self.localPackagesGroup = localPackagesGroup
self.preGenCommand = preGenCommand
self.postGenCommand = postGenCommand
self.useBaseInternationalization = useBaseInternationalization
self.schemePathPrefix = schemePathPrefix
}
}
extension SpecOptions: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let string: String = jsonDictionary.json(atKeyPath: "minimumXcodeGenVersion") {
minimumXcodeGenVersion = try Version.parse(string)
}
carthageBuildPath = jsonDictionary.json(atKeyPath: "carthageBuildPath")
carthageExecutablePath = jsonDictionary.json(atKeyPath: "carthageExecutablePath")
bundleIdPrefix = jsonDictionary.json(atKeyPath: "bundleIdPrefix")
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")
indentWidth = (jsonDictionary.json(atKeyPath: "indentWidth") as Int?).flatMap(UInt.init)
tabWidth = (jsonDictionary.json(atKeyPath: "tabWidth") as Int?).flatMap(UInt.init)
deploymentTarget = jsonDictionary.json(atKeyPath: "deploymentTarget") ?? DeploymentTarget()
disabledValidations = jsonDictionary.json(atKeyPath: "disabledValidations") ?? []
defaultConfig = jsonDictionary.json(atKeyPath: "defaultConfig")
transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies") ?? SpecOptions.transitivelyLinkDependenciesDefault
groupSortPosition = jsonDictionary.json(atKeyPath: "groupSortPosition") ?? SpecOptions.groupSortPositionDefault
groupOrdering = jsonDictionary.json(atKeyPath: "groupOrdering") ?? []
generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? SpecOptions.generateEmptyDirectoriesDefault
findCarthageFrameworks = jsonDictionary.json(atKeyPath: "findCarthageFrameworks") ?? SpecOptions.findCarthageFrameworksDefault
localPackagesGroup = jsonDictionary.json(atKeyPath: "localPackagesGroup")
preGenCommand = jsonDictionary.json(atKeyPath: "preGenCommand")
postGenCommand = jsonDictionary.json(atKeyPath: "postGenCommand")
useBaseInternationalization = jsonDictionary.json(atKeyPath: "useBaseInternationalization") ?? SpecOptions.useBaseInternationalizationDefault
schemePathPrefix = jsonDictionary.json(atKeyPath: "schemePathPrefix") ?? SpecOptions.schemePathPrefixDefault
if jsonDictionary["fileTypes"] != nil {
fileTypes = try jsonDictionary.json(atKeyPath: "fileTypes")
} else {
fileTypes = [:]
}
}
}
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?.description,
"carthageBuildPath": carthageBuildPath,
"carthageExecutablePath": carthageExecutablePath,
"bundleIdPrefix": bundleIdPrefix,
"developmentLanguage": developmentLanguage,
"usesTabs": usesTabs,
"xcodeVersion": xcodeVersion,
"indentWidth": indentWidth.flatMap { Int($0) },
"tabWidth": tabWidth.flatMap { Int($0) },
"defaultConfig": defaultConfig,
"localPackagesGroup": localPackagesGroup,
"preGenCommand": preGenCommand,
"postGenCommand": postGenCommand,
"fileTypes": fileTypes.mapValues { $0.toJSONValue() }
]
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
}
if useBaseInternationalization != SpecOptions.useBaseInternationalizationDefault {
dict["useBaseInternationalization"] = useBaseInternationalization
}
if schemePathPrefix != SpecOptions.schemePathPrefixDefault {
dict["schemePathPrefix"] = schemePathPrefix
}
return dict
}
}
extension SpecOptions: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("carthageBuildPath"),
]
}
}
@@ -0,0 +1,54 @@
import Foundation
public enum SpecParsingError: Error, CustomStringConvertible {
case unknownTargetType(String)
case unknownTargetPlatform(String)
case invalidDependency([String: Any])
case unknownPackageRequirement([String: Any])
case invalidSourceBuildPhase(String)
case invalidTargetReference(String)
case invalidTargetPlatformAsArray
case invalidVersion(String)
case unknownBreakpointType(String)
case unknownBreakpointScope(String)
case unknownBreakpointStopOnStyle(String)
case unknownBreakpointActionType(String)
case unknownBreakpointActionConveyanceType(String)
case unknownBreakpointActionSoundName(String)
case invalidConfigsMappingFormat(keys: Set<String>)
public var description: String {
switch self {
case let .unknownTargetType(type):
return "Unknown Target type: \(type)"
case let .unknownTargetPlatform(platform):
return "Unknown Target platform: \(platform)"
case let .invalidDependency(dependency):
return "Unknown Target dependency: \(dependency)"
case let .invalidSourceBuildPhase(error):
return "Invalid Source Build Phase: \(error)"
case let .invalidTargetReference(targetReference):
return "Invalid Target Reference Syntax: \(targetReference)"
case .invalidTargetPlatformAsArray:
return "Invalid Target platform: Array not allowed with supported destinations"
case let .invalidVersion(version):
return "Invalid version: \(version)"
case let .unknownPackageRequirement(package):
return "Unknown package requirement: \(package)"
case let .unknownBreakpointType(type):
return "Unknown Breakpoint type: \(type)"
case let .unknownBreakpointScope(scope):
return "Unknown Breakpoint scope: \(scope)"
case let .unknownBreakpointStopOnStyle(stopOnStyle):
return "Unknown Breakpoint stopOnStyle: \(stopOnStyle)"
case let .unknownBreakpointActionType(type):
return "Unknown Breakpoint Action type: \(type)"
case let .unknownBreakpointActionConveyanceType(type):
return "Unknown Breakpoint Action conveyance type: \(type)"
case let .unknownBreakpointActionSoundName(name):
return "Unknown Breakpoint Action sound name: \(name)"
case let .invalidConfigsMappingFormat(keys):
return "Invalid format: The value for \"\(keys.sorted().joined(separator: ", "))\" in `configs` must be mapping format"
}
}
}
@@ -0,0 +1,337 @@
import Foundation
import JSONUtilities
import PathKit
import Version
extension Project {
public func validate() throws {
var errors: [SpecValidationError.ValidationError] = []
func validateSettings(_ settings: Settings) -> [SpecValidationError.ValidationError] {
var errors: [SpecValidationError.ValidationError] = []
for group in settings.groups {
if let settings = settingGroups[group] {
errors += validateSettings(settings)
} else {
errors.append(.invalidSettingsGroup(group))
}
}
for config in settings.configSettings.keys {
if !configs.contains(where: { $0.name.lowercased().contains(config.lowercased()) }),
!options.disabledValidations.contains(.missingConfigs) {
errors.append(.invalidBuildSettingConfig(config))
}
}
if settings.buildSettings.count == configs.count {
var allConfigs = true
outerLoop: for buildSetting in settings.buildSettings.keys {
var isConfig = false
for config in configs {
if config.name.lowercased().contains(buildSetting.lowercased()) {
isConfig = true
break
}
}
if !isConfig {
allConfigs = false
break outerLoop
}
}
if allConfigs {
errors.append(.invalidPerConfigSettings)
}
}
return errors
}
errors += validateSettings(settings)
for fileGroup in fileGroups {
if !(basePath + fileGroup).exists {
errors.append(.invalidFileGroup(fileGroup))
}
}
for (name, package) in packages {
if case let .local(path, _, _) = package, !(basePath + Path(path).normalize()).exists {
errors.append(.invalidLocalPackage(name))
}
}
for (config, configFile) in configFiles {
if !options.disabledValidations.contains(.missingConfigFiles) && !(basePath + configFile).exists {
errors.append(.invalidConfigFile(configFile: configFile, config: config))
}
if !options.disabledValidations.contains(.missingConfigs) && getConfig(config) == nil {
errors.append(.invalidConfigFileConfig(config))
}
}
if let configName = options.defaultConfig {
if !configs.contains(where: { $0.name == configName }) {
errors.append(.missingDefaultConfig(configName: configName))
}
}
for settings in settingGroups.values {
errors += validateSettings(settings)
}
for target in projectTargets {
for (config, configFile) in target.configFiles {
let configPath = basePath + configFile
if !options.disabledValidations.contains(.missingConfigFiles) && !configPath.exists {
errors.append(.invalidTargetConfigFile(target: target.name, configFile: configPath.string, config: config))
}
if !options.disabledValidations.contains(.missingConfigs) && getConfig(config) == nil {
errors.append(.invalidConfigFileConfig(config))
}
}
if let scheme = target.scheme {
for configVariant in scheme.configVariants {
if configs.first(including: configVariant, for: .debug) == nil {
errors.append(.invalidTargetSchemeConfigVariant(
target: target.name,
configVariant: configVariant,
configType: .debug
))
}
if configs.first(including: configVariant, for: .release) == nil {
errors.append(.invalidTargetSchemeConfigVariant(
target: target.name,
configVariant: configVariant,
configType: .release
))
}
}
if scheme.configVariants.isEmpty {
if !configs.contains(where: { $0.type == .debug }) {
errors.append(.missingConfigForTargetScheme(target: target.name, configType: .debug))
}
if !configs.contains(where: { $0.type == .release }) {
errors.append(.missingConfigForTargetScheme(target: target.name, configType: .release))
}
}
for testTarget in scheme.testTargets {
if getTarget(testTarget.name) == nil {
// For test case of local Swift Package
if case .package(let name) = testTarget.targetReference.location, getPackage(name) != nil {
continue
}
errors.append(.invalidTargetSchemeTest(target: target.name, testTarget: testTarget.name))
}
}
if !options.disabledValidations.contains(.missingTestPlans) {
let invalidTestPlans: [TestPlan] = scheme.testPlans.filter { !(basePath + $0.path).exists }
errors.append(contentsOf: invalidTestPlans.map{ .invalidTestPlan($0) })
}
}
for script in target.buildScripts {
if case let .path(pathString) = script.script {
let scriptPath = basePath + pathString
if !scriptPath.exists {
errors.append(.invalidBuildScriptPath(target: target.name, name: script.name, path: scriptPath.string))
}
}
}
errors += validateSettings(target.settings)
for buildToolPlugin in target.buildToolPlugins {
if packages[buildToolPlugin.package] == nil {
errors.append(.invalidPluginPackageReference(plugin: buildToolPlugin.plugin, package: buildToolPlugin.package))
}
}
}
for target in aggregateTargets {
for dependency in target.targets {
if getProjectTarget(dependency) == nil {
errors.append(.invalidTargetDependency(target: target.name, dependency: dependency))
}
}
}
for target in targets {
var uniqueDependencies = Set<Dependency>()
for dependency in target.dependencies {
let dependencyValidationErrors = try validate(dependency, in: target)
errors.append(contentsOf: dependencyValidationErrors)
if uniqueDependencies.contains(dependency) {
errors.append(.duplicateDependencies(target: target.name, dependencyReference: dependency.reference))
} else {
uniqueDependencies.insert(dependency)
}
}
for source in target.sources {
let sourcePath = basePath + source.path
if !source.optional && !sourcePath.exists {
errors.append(.invalidTargetSource(target: target.name, source: sourcePath.string))
}
}
if target.supportedDestinations != nil, target.platform == .watchOS {
errors.append(.unexpectedTargetPlatformForSupportedDestinations(target: target.name, platform: target.platform))
}
if let supportedDestinations = target.supportedDestinations,
target.type.isApp,
supportedDestinations.contains(.watchOS) {
errors.append(.containsWatchOSDestinationForMultiplatformApp(target: target.name))
}
if target.supportedDestinations?.contains(.macOS) == true,
target.supportedDestinations?.contains(.macCatalyst) == true {
errors.append(.multipleMacPlatformsInSupportedDestinations(target: target.name))
}
if target.supportedDestinations?.contains(.macCatalyst) == true,
target.platform != .iOS, target.platform != .auto {
errors.append(.invalidTargetPlatformForSupportedDestinations(target: target.name))
}
if target.platform != .auto, target.platform != .watchOS,
let supportedDestination = SupportedDestination(rawValue: target.platform.rawValue),
target.supportedDestinations?.contains(supportedDestination) == false {
errors.append(.missingTargetPlatformInSupportedDestinations(target: target.name, platform: target.platform))
}
}
for projectReference in projectReferences {
if !(basePath + projectReference.path).exists {
errors.append(.invalidProjectReferencePath(projectReference))
}
}
for scheme in schemes {
errors.append(
contentsOf: scheme.build.targets.compactMap { validationError(for: $0.target, in: scheme, action: "build") }
)
if let action = scheme.run, let config = action.config, getConfig(config) == nil {
errors.append(.invalidSchemeConfig(scheme: scheme.name, config: config))
}
if !options.disabledValidations.contains(.missingTestPlans) {
let invalidTestPlans: [TestPlan] = scheme.test?.testPlans.filter { !(basePath + $0.path).exists } ?? []
errors.append(contentsOf: invalidTestPlans.map{ .invalidTestPlan($0) })
}
let defaultPlanCount = scheme.test?.testPlans.filter { $0.defaultPlan }.count ?? 0
if (defaultPlanCount > 1) {
errors.append(.multipleDefaultTestPlans)
}
if let action = scheme.test, let config = action.config, getConfig(config) == nil {
errors.append(.invalidSchemeConfig(scheme: scheme.name, config: config))
}
errors.append(
contentsOf: scheme.test?.targets.compactMap { validationError(for: $0.targetReference, in: scheme, action: "test") } ?? []
)
errors.append(
contentsOf: scheme.test?.coverageTargets.compactMap { validationError(for: $0, in: scheme, action: "test") } ?? []
)
if let action = scheme.profile, let config = action.config, getConfig(config) == nil {
errors.append(.invalidSchemeConfig(scheme: scheme.name, config: config))
}
if let action = scheme.analyze, let config = action.config, getConfig(config) == nil {
errors.append(.invalidSchemeConfig(scheme: scheme.name, config: config))
}
if let action = scheme.archive, let config = action.config, getConfig(config) == nil {
errors.append(.invalidSchemeConfig(scheme: scheme.name, config: config))
}
}
if !errors.isEmpty {
throw SpecValidationError(errors: errors)
}
}
public func validateMinimumXcodeGenVersion(_ xcodeGenVersion: Version) throws {
if let minimumXcodeGenVersion = options.minimumXcodeGenVersion, xcodeGenVersion < minimumXcodeGenVersion {
throw SpecValidationError(errors: [SpecValidationError.ValidationError.invalidXcodeGenVersion(minimumVersion: minimumXcodeGenVersion, version: xcodeGenVersion)])
}
}
// Returns error if the given dependency from target is invalid.
private func validate(_ dependency: Dependency, in target: Target) throws -> [SpecValidationError.ValidationError] {
var errors: [SpecValidationError.ValidationError] = []
switch dependency.type {
case .target:
let dependencyTargetReference = try TargetReference(dependency.reference)
switch dependencyTargetReference.location {
case .local:
if getProjectTarget(dependency.reference) == nil {
errors.append(.invalidTargetDependency(target: target.name, dependency: dependency.reference))
}
case .project(let dependencyProjectName):
if getProjectReference(dependencyProjectName) == nil {
errors.append(.invalidTargetDependency(target: target.name, dependency: dependency.reference))
}
}
case .sdk:
let path = Path(dependency.reference)
if !dependency.reference.contains("/") {
switch path.extension {
case "framework"?,
"tbd"?,
"dylib"?:
break
default:
errors.append(.invalidSDKDependency(target: target.name, dependency: dependency.reference))
}
}
case .package:
if packages[dependency.reference] == nil {
errors.append(.invalidSwiftPackage(name: dependency.reference, target: target.name))
}
default: break
}
return errors
}
/// Returns a descriptive error if the given target reference was invalid otherwise `nil`.
private func validationError(for targetReference: TargetReference, in scheme: Scheme, action: String) -> SpecValidationError.ValidationError? {
switch targetReference.location {
case .local where getProjectTarget(targetReference.name) == nil:
return .invalidSchemeTarget(scheme: scheme.name, target: targetReference.name, action: action)
case .project(let project) where getProjectReference(project) == nil:
return .invalidProjectReference(scheme: scheme.name, reference: project)
case .local, .project:
return nil
}
}
/// Returns a descriptive error if the given target reference was invalid otherwise `nil`.
private func validationError(for testableTargetReference: TestableTargetReference, in scheme: Scheme, action: String) -> SpecValidationError.ValidationError? {
switch testableTargetReference.location {
case .local where getProjectTarget(testableTargetReference.name) == nil:
return .invalidSchemeTarget(scheme: scheme.name, target: testableTargetReference.name, action: action)
case .project(let project) where getProjectReference(project) == nil:
return .invalidProjectReference(scheme: scheme.name, reference: project)
case .package(let package) where getPackage(package) == nil:
return .invalidLocalPackage(package)
case .local, .project, .package:
return nil
}
}
}
@@ -0,0 +1,125 @@
import Foundation
import Version
public struct SpecValidationError: Error, CustomStringConvertible {
public var errors: [ValidationError]
public init(errors: [ValidationError]) {
self.errors = errors
}
public enum ValidationError: Hashable, Error, CustomStringConvertible {
case invalidXcodeGenVersion(minimumVersion: Version, version: Version)
case invalidSDKDependency(target: String, dependency: String)
case invalidTargetDependency(target: String, dependency: String)
case invalidTargetSource(target: String, source: String)
case invalidTargetConfigFile(target: String, configFile: String, config: String)
case invalidTargetSchemeConfigVariant(target: String, configVariant: String, configType: ConfigType)
case invalidTargetSchemeTest(target: String, testTarget: String)
case invalidTargetPlatformForSupportedDestinations(target: String)
case unexpectedTargetPlatformForSupportedDestinations(target: String, platform: Platform)
case containsWatchOSDestinationForMultiplatformApp(target: String)
case multipleMacPlatformsInSupportedDestinations(target: String)
case missingTargetPlatformInSupportedDestinations(target: String, platform: Platform)
case invalidSchemeTarget(scheme: String, target: String, action: String)
case invalidSchemeConfig(scheme: String, config: String)
case invalidSwiftPackage(name: String, target: String)
case invalidPackageDependencyReference(name: String)
case invalidLocalPackage(String)
case invalidConfigFile(configFile: String, config: String)
case invalidBuildSettingConfig(String)
case invalidSettingsGroup(String)
case invalidBuildScriptPath(target: String, name: String?, path: String)
case invalidFileGroup(String)
case invalidConfigFileConfig(String)
case missingConfigForTargetScheme(target: String, configType: ConfigType)
case missingDefaultConfig(configName: String)
case invalidPerConfigSettings
case invalidProjectReference(scheme: String, reference: String)
case invalidProjectReferencePath(ProjectReference)
case invalidTestPlan(TestPlan)
case multipleDefaultTestPlans
case duplicateDependencies(target: String, dependencyReference: String)
case invalidPluginPackageReference(plugin: String, package: String)
public var description: String {
switch self {
case let .invalidXcodeGenVersion(minimumVersion, version):
return "XcodeGen version is \(version), but minimum required version specified as \(minimumVersion)"
case let .invalidSDKDependency(target, dependency):
return "Target \(target.quoted) has invalid sdk dependency: \(dependency.quoted). It must be a full path or have the following extensions: .framework, .dylib, .tbd"
case let .invalidTargetDependency(target, dependency):
return "Target \(target.quoted) has invalid dependency: \(dependency.quoted)"
case let .invalidTargetConfigFile(target, configFile, config):
return "Target \(target.quoted) has invalid config file path \(configFile.quoted) for config \(config.quoted)"
case let .invalidTargetSource(target, source):
return "Target \(target.quoted) has a missing source directory \(source.quoted)"
case let .invalidTargetSchemeConfigVariant(target, configVariant, configType):
return "Target \(target.quoted) has an invalid scheme config variant which requires a config that has a \(configType.rawValue.quoted) type and contains the name \(configVariant.quoted)"
case let .invalidTargetSchemeTest(target, test):
return "Target \(target.quoted) scheme has invalid test \(test.quoted)"
case let .invalidTargetPlatformForSupportedDestinations(target):
return "Target \(target.quoted) has supported destinations that require a target platform iOS or auto"
case let .unexpectedTargetPlatformForSupportedDestinations(target, platform):
return "Target \(target.quoted) has platform \(platform.rawValue.quoted) that does not expect supported destinations"
case let .multipleMacPlatformsInSupportedDestinations(target):
return "Target \(target.quoted) has multiple definitions of mac platforms in supported destinations"
case let .missingTargetPlatformInSupportedDestinations(target, platform):
return "Target \(target.quoted) has platform \(platform.rawValue.quoted) that is missing in supported destinations"
case let .containsWatchOSDestinationForMultiplatformApp(target):
return "Multiplatform app \(target.quoted) cannot contain watchOS in \"supportedDestinations\". Create a separate target using \"platform\" for watchOS apps"
case let .invalidConfigFile(configFile, config):
return "Invalid config file \(configFile.quoted) for config \(config.quoted)"
case let .invalidSchemeTarget(scheme, target, action):
return "Scheme \(scheme.quoted) has invalid \(action) target \(target.quoted)"
case let .invalidSchemeConfig(scheme, config):
return "Scheme \(scheme.quoted) has invalid build configuration \(config.quoted)"
case let .invalidBuildSettingConfig(config):
return "Build setting has invalid build configuration \(config.quoted)"
case let .invalidSettingsGroup(group):
return "Invalid settings group \(group.quoted)"
case let .invalidBuildScriptPath(target, name, path):
return "Target \(target.quoted) has a script \(name != nil ? "\(name!.quoted) which has a " : "")path that doesn't exist \(path.quoted)"
case let .invalidFileGroup(group):
return "Invalid file group \(group.quoted)"
case let .invalidConfigFileConfig(config):
return "Config file has invalid config \(config.quoted)"
case let .invalidSwiftPackage(name, target):
return "Target \(target.quoted) has an invalid package dependency \(name.quoted)"
case let .invalidLocalPackage(path):
return "Invalid local package \(path.quoted)"
case let .invalidPackageDependencyReference(name):
return "Package reference \(name) must be specified as package dependency, not target"
case let .missingConfigForTargetScheme(target, configType):
return "Target \(target.quoted) is missing a config of type \(configType.rawValue) to generate its scheme"
case let .missingDefaultConfig(name):
return "Default configuration \(name) doesn't exist"
case .invalidPerConfigSettings:
return "Settings that are for a specific config must go in \"configs\". \"base\" can be used for common settings"
case let .invalidProjectReference(scheme, project):
return "Scheme \(scheme.quoted) has invalid project reference \(project.quoted)"
case let .invalidProjectReferencePath(reference):
return "Project reference \(reference.name) has a project file path that doesn't exist \"\(reference.path)\""
case let .invalidTestPlan(testPlan):
return "Test plan path \"\(testPlan.path)\" doesn't exist"
case .multipleDefaultTestPlans:
return "Your test plans contain more than one default test plan"
case let .duplicateDependencies(target, dependencyReference):
return "Target \(target.quoted) has the dependency \(dependencyReference.quoted) multiple times"
case let .invalidPluginPackageReference(plugin, package):
return "Plugin \(plugin) has invalid package reference \(package)"
}
}
}
public var description: String {
let title: String
if errors.count == 1 {
title = "Spec validation error: "
} else {
title = "\(errors.count) Spec validations errors:\n\t- "
}
return "\(title)" + errors.map { $0.description }.joined(separator: "\n\t- ")
}
}
@@ -0,0 +1,50 @@
import Foundation
public enum SupportedDestination: String, CaseIterable {
case iOS
case tvOS
case macOS
case macCatalyst
case watchOS
case visionOS
}
extension SupportedDestination {
public var string: String {
switch self {
case .iOS:
return "ios"
case .tvOS:
return "tvos"
case .macOS:
return "macos"
case .macCatalyst:
return "maccatalyst"
case .watchOS:
return "watchos"
case .visionOS:
return "xros"
}
}
// This is used to:
// 1. Get the first one and apply SettingPresets 'Platforms' and 'Product_Platform' if the platform is 'auto'
// 2. Sort, loop and merge together SettingPresets 'SupportedDestinations'
public var priority: Int {
switch self {
case .iOS:
return 0
case .tvOS:
return 1
case .watchOS:
return 2
case .visionOS:
return 3
case .macOS:
return 4
case .macCatalyst:
return 5
}
}
}
@@ -0,0 +1,140 @@
import Foundation
import XcodeProj
import JSONUtilities
import Version
public enum SwiftPackage: Equatable {
public typealias VersionRequirement = XCRemoteSwiftPackageReference.VersionRequirement
static let githubPrefix = "https://github.com/"
case remote(url: String, versionRequirement: VersionRequirement)
case local(path: String, group: String?, excludeFromProject: Bool)
public var isLocal: Bool {
if case .local = self {
return true
}
return false
}
}
extension SwiftPackage: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let path: String = jsonDictionary.json(atKeyPath: "path") {
let customLocation: String? = jsonDictionary.json(atKeyPath: "group")
let excludeFromProject: Bool = jsonDictionary.json(atKeyPath: "excludeFromProject") ?? false
self = .local(path: path, group: customLocation, excludeFromProject: excludeFromProject)
} else {
let versionRequirement: VersionRequirement = try VersionRequirement(jsonDictionary: jsonDictionary)
try Self.validateVersion(versionRequirement: versionRequirement)
let url: String
if jsonDictionary["github"] != nil {
let github: String = try jsonDictionary.json(atKeyPath: "github")
url = "\(Self.githubPrefix)\(github)"
} else {
url = try jsonDictionary.json(atKeyPath: "url")
}
self = .remote(url: url, versionRequirement: versionRequirement)
}
}
private static func validateVersion(versionRequirement: VersionRequirement) throws {
switch versionRequirement {
case .upToNextMajorVersion(let version):
try _ = Version.parse(version)
case .upToNextMinorVersion(let version):
try _ = Version.parse(version)
case .range(let from, let to):
try _ = Version.parse(from)
try _ = Version.parse(to)
case .exact(let version):
try _ = Version.parse(version)
default:
break
}
}
}
extension SwiftPackage: JSONEncodable {
public func toJSONValue() -> Any {
var dictionary: JSONDictionary = [:]
switch self {
case .remote(let url, let versionRequirement):
if url.hasPrefix(Self.githubPrefix) {
dictionary["github"] = url.replacingOccurrences(of: Self.githubPrefix, with: "")
} else {
dictionary["url"] = url
}
switch versionRequirement {
case .upToNextMajorVersion(let version):
dictionary["majorVersion"] = version
case .upToNextMinorVersion(let version):
dictionary["minorVersion"] = version
case .range(let from, let to):
dictionary["minVersion"] = from
dictionary["maxVersion"] = to
case .exact(let version):
dictionary["exactVersion"] = version
case .branch(let branch):
dictionary["branch"] = branch
case .revision(let revision):
dictionary["revision"] = revision
}
return dictionary
case let .local(path, group, excludeFromProject):
dictionary["path"] = path
dictionary["group"] = group
dictionary["excludeFromProject"] = excludeFromProject
}
return dictionary
}
}
extension SwiftPackage.VersionRequirement: JSONUtilities.JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if jsonDictionary["exactVersion"] != nil {
self = try .exact(jsonDictionary.json(atKeyPath: "exactVersion"))
} else if jsonDictionary["version"] != nil {
self = try .exact(jsonDictionary.json(atKeyPath: "version"))
} else if jsonDictionary["revision"] != nil {
self = try .revision(jsonDictionary.json(atKeyPath: "revision"))
} else if jsonDictionary["branch"] != nil {
self = try .branch(jsonDictionary.json(atKeyPath: "branch"))
} else if jsonDictionary["minVersion"] != nil && jsonDictionary["maxVersion"] != nil {
let minimum: String = try jsonDictionary.json(atKeyPath: "minVersion")
let maximum: String = try jsonDictionary.json(atKeyPath: "maxVersion")
self = .range(from: minimum, to: maximum)
} else if jsonDictionary["minorVersion"] != nil {
self = try .upToNextMinorVersion(jsonDictionary.json(atKeyPath: "minorVersion"))
} else if jsonDictionary["majorVersion"] != nil {
self = try .upToNextMajorVersion(jsonDictionary.json(atKeyPath: "majorVersion"))
} else if jsonDictionary["from"] != nil {
self = try .upToNextMajorVersion(jsonDictionary.json(atKeyPath: "from"))
} else {
throw SpecParsingError.unknownPackageRequirement(jsonDictionary)
}
}
}
extension SwiftPackage: PathContainer {
static var pathProperties: [PathProperty] {
[
.dictionary([
.string("path"),
]),
]
}
}
+416
View File
@@ -0,0 +1,416 @@
import Foundation
import JSONUtilities
import XcodeProj
import Version
public struct LegacyTarget: Equatable {
public static let passSettingsDefault = false
public var toolPath: String
public var arguments: String?
public var passSettings: Bool
public var workingDirectory: String?
public init(
toolPath: String,
passSettings: Bool = passSettingsDefault,
arguments: String? = nil,
workingDirectory: String? = nil
) {
self.toolPath = toolPath
self.arguments = arguments
self.passSettings = passSettings
self.workingDirectory = workingDirectory
}
}
extension LegacyTarget: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("workingDirectory"),
]
}
}
public struct Target: ProjectTarget {
public var name: String
public var type: PBXProductType
public var platform: Platform
public var supportedDestinations: [SupportedDestination]?
public var settings: Settings
public var sources: [TargetSource]
public var dependencies: [Dependency]
public var info: Plist?
public var entitlements: Plist?
public var transitivelyLinkDependencies: Bool?
public var directlyEmbedCarthageDependencies: Bool?
public var requiresObjCLinking: Bool?
public var preBuildScripts: [BuildScript]
public var buildToolPlugins: [BuildToolPlugin]
public var postCompileScripts: [BuildScript]
public var postBuildScripts: [BuildScript]
public var buildRules: [BuildRule]
public var configFiles: [String: String]
public var scheme: TargetScheme?
public var legacy: LegacyTarget?
public var deploymentTarget: Version?
public var attributes: [String: Any]
public var productName: String
public var onlyCopyFilesOnInstall: Bool
public var putResourcesBeforeSourcesBuildPhase: Bool
public var isLegacy: Bool {
legacy != nil
}
public var filename: String {
var filename = productName
if let fileExtension = type.fileExtension {
filename += ".\(fileExtension)"
}
if type == .staticLibrary {
filename = "lib\(filename)"
}
return filename
}
public init(
name: String,
type: PBXProductType,
platform: Platform,
supportedDestinations: [SupportedDestination]? = nil,
productName: String? = nil,
deploymentTarget: Version? = nil,
settings: Settings = .empty,
configFiles: [String: String] = [:],
sources: [TargetSource] = [],
dependencies: [Dependency] = [],
info: Plist? = nil,
entitlements: Plist? = nil,
transitivelyLinkDependencies: Bool? = nil,
directlyEmbedCarthageDependencies: Bool? = nil,
requiresObjCLinking: Bool? = nil,
preBuildScripts: [BuildScript] = [],
buildToolPlugins: [BuildToolPlugin] = [],
postCompileScripts: [BuildScript] = [],
postBuildScripts: [BuildScript] = [],
buildRules: [BuildRule] = [],
scheme: TargetScheme? = nil,
legacy: LegacyTarget? = nil,
attributes: [String: Any] = [:],
onlyCopyFilesOnInstall: Bool = false,
putResourcesBeforeSourcesBuildPhase: Bool = false
) {
self.name = name
self.type = type
self.platform = platform
self.supportedDestinations = supportedDestinations
self.deploymentTarget = deploymentTarget
self.productName = productName ?? name
self.settings = settings
self.configFiles = configFiles
self.sources = sources
self.dependencies = dependencies
self.info = info
self.entitlements = entitlements
self.transitivelyLinkDependencies = transitivelyLinkDependencies
self.directlyEmbedCarthageDependencies = directlyEmbedCarthageDependencies
self.requiresObjCLinking = requiresObjCLinking
self.preBuildScripts = preBuildScripts
self.buildToolPlugins = buildToolPlugins
self.postCompileScripts = postCompileScripts
self.postBuildScripts = postBuildScripts
self.buildRules = buildRules
self.scheme = scheme
self.legacy = legacy
self.attributes = attributes
self.onlyCopyFilesOnInstall = onlyCopyFilesOnInstall
self.putResourcesBeforeSourcesBuildPhase = putResourcesBeforeSourcesBuildPhase
}
}
extension Target: CustomStringConvertible {
public var description: String {
"\(name): \(platform.rawValue) \(type)"
}
}
extension Target: PathContainer {
static var pathProperties: [PathProperty] {
[
.dictionary([
.string("sources"),
.object("sources", TargetSource.pathProperties),
.string("configFiles"),
.object("dependencies", Dependency.pathProperties),
.object("info", Plist.pathProperties),
.object("entitlements", Plist.pathProperties),
.object("preBuildScripts", BuildScript.pathProperties),
.object("prebuildScripts", BuildScript.pathProperties),
.object("postCompileScripts", BuildScript.pathProperties),
.object("postBuildScripts", BuildScript.pathProperties),
.object("legacy", LegacyTarget.pathProperties),
.object("scheme", TargetScheme.pathProperties),
]),
]
}
}
extension Target {
static func resolveMultiplatformTargets(jsonDictionary: JSONDictionary) -> JSONDictionary {
guard let targetsDictionary: [String: JSONDictionary] = jsonDictionary["targets"] as? [String: JSONDictionary] else {
return jsonDictionary
}
var crossPlatformTargets: [String: JSONDictionary] = [:]
for (targetName, target) in targetsDictionary {
if let platforms = target["platform"] as? [String] {
for platform in platforms {
var platformTarget = target
/// This value is set to help us to check, in Target init, that there are no conflicts in the definition of the platforms. We want to ensure that the user didn't define, at the same time,
/// the new Xcode 14 supported destinations and the XcodeGen generation of Multiple Platform Targets (when you define the platform field as an array).
platformTarget["isMultiPlatformTarget"] = true
platformTarget = platformTarget.expand(variables: ["platform": platform])
platformTarget["platform"] = platform
let platformSuffix = platformTarget["platformSuffix"] as? String ?? "_\(platform)"
let platformPrefix = platformTarget["platformPrefix"] as? String ?? ""
let newTargetName = platformPrefix + targetName + platformSuffix
var settings = platformTarget["settings"] as? JSONDictionary ?? [:]
if settings["configs"] != nil || settings["groups"] != nil || settings["base"] != nil {
var base = settings["base"] as? JSONDictionary ?? [:]
if base["PRODUCT_NAME"] == nil {
base["PRODUCT_NAME"] = targetName
}
settings["base"] = base
} else {
if settings["PRODUCT_NAME"] == nil {
settings["PRODUCT_NAME"] = targetName
}
}
platformTarget["productName"] = targetName
platformTarget["settings"] = settings
if let deploymentTargets = target["deploymentTarget"] as? [String: Any] {
platformTarget["deploymentTarget"] = deploymentTargets[platform]
}
crossPlatformTargets[newTargetName] = platformTarget
}
} else {
crossPlatformTargets[targetName] = target
}
}
var merged = jsonDictionary
merged["targets"] = crossPlatformTargets
return merged
}
}
extension Target: Equatable {
public static func == (lhs: Target, rhs: Target) -> Bool {
lhs.name == rhs.name &&
lhs.type == rhs.type &&
lhs.platform == rhs.platform &&
lhs.deploymentTarget == rhs.deploymentTarget &&
lhs.transitivelyLinkDependencies == rhs.transitivelyLinkDependencies &&
lhs.requiresObjCLinking == rhs.requiresObjCLinking &&
lhs.directlyEmbedCarthageDependencies == rhs.directlyEmbedCarthageDependencies &&
lhs.settings == rhs.settings &&
lhs.configFiles == rhs.configFiles &&
lhs.sources == rhs.sources &&
lhs.info == rhs.info &&
lhs.entitlements == rhs.entitlements &&
lhs.dependencies == rhs.dependencies &&
lhs.preBuildScripts == rhs.preBuildScripts &&
lhs.buildToolPlugins == rhs.buildToolPlugins &&
lhs.postCompileScripts == rhs.postCompileScripts &&
lhs.postBuildScripts == rhs.postBuildScripts &&
lhs.buildRules == rhs.buildRules &&
lhs.scheme == rhs.scheme &&
lhs.legacy == rhs.legacy &&
NSDictionary(dictionary: lhs.attributes).isEqual(to: rhs.attributes)
}
}
extension LegacyTarget: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
toolPath = try jsonDictionary.json(atKeyPath: "toolPath")
arguments = jsonDictionary.json(atKeyPath: "arguments")
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 {
let resolvedName: String = jsonDictionary.json(atKeyPath: "name") ?? name
self.name = resolvedName
productName = jsonDictionary.json(atKeyPath: "productName") ?? resolvedName
let typeString: String = jsonDictionary.json(atKeyPath: "type") ?? ""
if let type = PBXProductType(string: typeString) {
self.type = type
} else {
throw SpecParsingError.unknownTargetType(typeString)
}
if let supportedDestinations: [SupportedDestination] = jsonDictionary.json(atKeyPath: "supportedDestinations") {
self.supportedDestinations = supportedDestinations
}
let isResolved = jsonDictionary.json(atKeyPath: "isMultiPlatformTarget") ?? false
if isResolved, supportedDestinations != nil {
throw SpecParsingError.invalidTargetPlatformAsArray
}
var platformString: String = jsonDictionary.json(atKeyPath: "platform") ?? ""
// platform defaults to 'auto' if it is empty and we are using supported destinations
if supportedDestinations != nil, platformString.isEmpty {
platformString = Platform.auto.rawValue
}
// we add 'iOS' in supported destinations if it contains only 'macCatalyst'
if supportedDestinations?.contains(.macCatalyst) == true,
supportedDestinations?.contains(.iOS) == false {
supportedDestinations?.append(.iOS)
}
if let platform = Platform(rawValue: platformString) {
self.platform = platform
} else {
throw SpecParsingError.unknownTargetPlatform(platformString)
}
if let string: String = jsonDictionary.json(atKeyPath: "deploymentTarget") {
deploymentTarget = try Version.parse(string)
} else if let double: Double = jsonDictionary.json(atKeyPath: "deploymentTarget") {
deploymentTarget = try Version.parse(String(double))
} else {
deploymentTarget = nil
}
settings = try BuildSettingsParser(jsonDictionary: jsonDictionary).parse()
configFiles = jsonDictionary.json(atKeyPath: "configFiles") ?? [:]
if let source: String = jsonDictionary.json(atKeyPath: "sources") {
sources = [TargetSource(path: source)]
} else if let array = jsonDictionary["sources"] as? [Any] {
sources = try array.compactMap { source in
if let string = source as? String {
return TargetSource(path: string)
} else if let dictionary = source as? [String: Any] {
return try TargetSource(jsonDictionary: dictionary)
} else {
return nil
}
}
} else {
sources = []
}
if jsonDictionary["dependencies"] == nil {
dependencies = []
} else {
let dependencies: [Dependency] = try jsonDictionary.json(atKeyPath: "dependencies", invalidItemBehaviour: .fail)
self.dependencies = dependencies.filter { [platform] dependency -> Bool in
// If unspecified, all platforms are supported
guard let platforms = dependency.platforms else { return true }
return platforms.contains(platform)
}
}
if jsonDictionary["buildToolPlugins"] == nil {
buildToolPlugins = []
} else {
self.buildToolPlugins = try jsonDictionary.json(atKeyPath: "buildToolPlugins", invalidItemBehaviour: .fail)
}
if jsonDictionary["info"] != nil {
info = try jsonDictionary.json(atKeyPath: "info") as Plist
}
if jsonDictionary["entitlements"] != nil {
entitlements = try jsonDictionary.json(atKeyPath: "entitlements") as Plist
}
transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies")
directlyEmbedCarthageDependencies = jsonDictionary.json(atKeyPath: "directlyEmbedCarthageDependencies")
requiresObjCLinking = jsonDictionary.json(atKeyPath: "requiresObjCLinking")
preBuildScripts = jsonDictionary.json(atKeyPath: "preBuildScripts") ?? jsonDictionary.json(atKeyPath: "prebuildScripts") ?? []
postCompileScripts = jsonDictionary.json(atKeyPath: "postCompileScripts") ?? []
postBuildScripts = jsonDictionary.json(atKeyPath: "postBuildScripts") ?? jsonDictionary.json(atKeyPath: "postbuildScripts") ?? []
buildRules = jsonDictionary.json(atKeyPath: "buildRules") ?? []
scheme = jsonDictionary.json(atKeyPath: "scheme")
legacy = jsonDictionary.json(atKeyPath: "legacy")
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [:]
onlyCopyFilesOnInstall = jsonDictionary.json(atKeyPath: "onlyCopyFilesOnInstall") ?? false
putResourcesBeforeSourcesBuildPhase = jsonDictionary.json(atKeyPath: "putResourcesBeforeSourcesBuildPhase") ?? false
}
}
extension Target: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"type": type.name,
"platform": platform.rawValue,
"supportedDestinations": supportedDestinations?.map { $0.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() },
"buildToolPlugins": buildToolPlugins.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
}
if onlyCopyFilesOnInstall {
dict["onlyCopyFilesOnInstall"] = true
}
if putResourcesBeforeSourcesBuildPhase {
dict["putResourcesBeforeSourcesBuildPhase"] = true
}
return dict
}
}
@@ -0,0 +1,57 @@
import Foundation
import JSONUtilities
public struct TargetReference: Hashable {
public var name: String
public var location: Location
public enum Location: Hashable {
case local
case project(String)
}
public init(name: String, location: Location) {
self.name = name
self.location = location
}
}
extension TargetReference {
public init(_ string: String) throws {
let paths = string.split(separator: "/")
switch paths.count {
case 2:
location = .project(String(paths[0]))
name = String(paths[1])
case 1:
location = .local
name = String(paths[0])
default:
throw SpecParsingError.invalidTargetReference(string)
}
}
public static func local(_ name: String) -> TargetReference {
TargetReference(name: name, location: .local)
}
}
extension TargetReference: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
try! self.init(value)
}
}
extension TargetReference: CustomStringConvertible {
public var reference: String {
switch location {
case .local: return name
case .project(let root):
return "\(root)/\(name)"
}
}
public var description: String {
reference
}
}
@@ -0,0 +1,181 @@
import Foundation
import JSONUtilities
import XcodeProj
public struct TargetScheme: Equatable {
public static let gatherCoverageDataDefault = false
public static let disableMainThreadCheckerDefault = false
public static let stopOnEveryMainThreadCheckerIssueDefault = false
public static let disableThreadPerformanceCheckerDefault = false
public static let buildImplicitDependenciesDefault = true
public var testTargets: [Scheme.Test.TestTarget]
public var configVariants: [String]
public var gatherCoverageData: Bool
public var coverageTargets: [TestableTargetReference]
public var storeKitConfiguration: String?
public var language: String?
public var region: String?
public var disableMainThreadChecker: Bool
public var stopOnEveryMainThreadCheckerIssue: Bool
public var disableThreadPerformanceChecker: Bool
public var buildImplicitDependencies: Bool
public var commandLineArguments: [String: Bool]
public var environmentVariables: [XCScheme.EnvironmentVariable]
public var preActions: [Scheme.ExecutionAction]
public var postActions: [Scheme.ExecutionAction]
public var management: Scheme.Management?
public var testPlans: [TestPlan]
public init(
testTargets: [Scheme.Test.TestTarget] = [],
testPlans: [TestPlan] = [],
configVariants: [String] = [],
gatherCoverageData: Bool = gatherCoverageDataDefault,
coverageTargets: [TestableTargetReference] = [],
storeKitConfiguration: String? = nil,
language: String? = nil,
region: String? = nil,
disableMainThreadChecker: Bool = disableMainThreadCheckerDefault,
stopOnEveryMainThreadCheckerIssue: Bool = stopOnEveryMainThreadCheckerIssueDefault,
disableThreadPerformanceChecker: Bool = disableThreadPerformanceCheckerDefault,
buildImplicitDependencies: Bool = buildImplicitDependenciesDefault,
commandLineArguments: [String: Bool] = [:],
environmentVariables: [XCScheme.EnvironmentVariable] = [],
preActions: [Scheme.ExecutionAction] = [],
postActions: [Scheme.ExecutionAction] = [],
management: Scheme.Management? = nil
) {
self.testTargets = testTargets
self.testPlans = testPlans
self.configVariants = configVariants
self.gatherCoverageData = gatherCoverageData
self.coverageTargets = coverageTargets
self.storeKitConfiguration = storeKitConfiguration
self.language = language
self.region = region
self.disableMainThreadChecker = disableMainThreadChecker
self.stopOnEveryMainThreadCheckerIssue = stopOnEveryMainThreadCheckerIssue
self.disableThreadPerformanceChecker = disableThreadPerformanceChecker
self.buildImplicitDependencies = buildImplicitDependencies
self.commandLineArguments = commandLineArguments
self.environmentVariables = environmentVariables
self.preActions = preActions
self.postActions = postActions
self.postActions = postActions
self.management = management
}
}
extension TargetScheme: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let targets = jsonDictionary["testTargets"] as? [Any] {
testTargets = try targets.compactMap { target in
if let string = target as? String {
return .init(targetReference: try TestableTargetReference(string))
} else if let dictionary = target as? JSONDictionary,
let target: Scheme.Test.TestTarget = try? .init(jsonDictionary: dictionary) {
return target
} else {
return nil
}
}
} else {
testTargets = []
}
if let targets = jsonDictionary["coverageTargets"] as? [Any] {
coverageTargets = try targets.compactMap { target in
if let string = target as? String {
return try TestableTargetReference(string)
} else if let dictionary = target as? JSONDictionary,
let target: TestableTargetReference = try? .init(jsonDictionary: dictionary) {
return target
} else {
return nil
}
}
} else {
coverageTargets = []
}
testPlans = try (jsonDictionary.json(atKeyPath: "testPlans") ?? []).map { try TestPlan(jsonDictionary: $0) }
configVariants = jsonDictionary.json(atKeyPath: "configVariants") ?? []
gatherCoverageData = jsonDictionary.json(atKeyPath: "gatherCoverageData") ?? TargetScheme.gatherCoverageDataDefault
storeKitConfiguration = jsonDictionary.json(atKeyPath: "storeKitConfiguration")
language = jsonDictionary.json(atKeyPath: "language")
region = jsonDictionary.json(atKeyPath: "region")
disableMainThreadChecker = jsonDictionary.json(atKeyPath: "disableMainThreadChecker") ?? TargetScheme.disableMainThreadCheckerDefault
stopOnEveryMainThreadCheckerIssue = jsonDictionary.json(atKeyPath: "stopOnEveryMainThreadCheckerIssue") ?? TargetScheme.stopOnEveryMainThreadCheckerIssueDefault
disableThreadPerformanceChecker = jsonDictionary.json(atKeyPath: "disableThreadPerformanceChecker") ?? TargetScheme.disableThreadPerformanceCheckerDefault
buildImplicitDependencies = jsonDictionary.json(atKeyPath: "buildImplicitDependencies") ?? TargetScheme.buildImplicitDependenciesDefault
commandLineArguments = jsonDictionary.json(atKeyPath: "commandLineArguments") ?? [:]
environmentVariables = try XCScheme.EnvironmentVariable.parseAll(jsonDictionary: jsonDictionary)
preActions = jsonDictionary.json(atKeyPath: "preActions") ?? []
postActions = jsonDictionary.json(atKeyPath: "postActions") ?? []
management = jsonDictionary.json(atKeyPath: "management")
}
}
extension TargetScheme: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any] = [
"configVariants": configVariants,
"coverageTargets": coverageTargets.map { $0.reference },
"commandLineArguments": commandLineArguments,
"testTargets": testTargets.map { $0.toJSONValue() },
"testPlans": testPlans.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
}
if let storeKitConfiguration = storeKitConfiguration {
dict["storeKitConfiguration"] = storeKitConfiguration
}
if disableMainThreadChecker != TargetScheme.disableMainThreadCheckerDefault {
dict["disableMainThreadChecker"] = disableMainThreadChecker
}
if stopOnEveryMainThreadCheckerIssue != TargetScheme.stopOnEveryMainThreadCheckerIssueDefault {
dict["stopOnEveryMainThreadCheckerIssue"] = stopOnEveryMainThreadCheckerIssue
}
if disableThreadPerformanceChecker != TargetScheme.disableThreadPerformanceCheckerDefault {
dict["disableThreadPerformanceChecker"] = disableThreadPerformanceChecker
}
if buildImplicitDependencies != TargetScheme.buildImplicitDependenciesDefault {
dict["buildImplicitDependencies"] = buildImplicitDependencies
}
if let language = language {
dict["language"] = language
}
if let region = region {
dict["region"] = region
}
if let management = management {
dict["management"] = management.toJSONValue()
}
return dict
}
}
extension TargetScheme: PathContainer {
static var pathProperties: [PathProperty] {
[
.object("testPlans", TestPlan.pathProperties),
]
}
}
@@ -0,0 +1,163 @@
import Foundation
import JSONUtilities
import PathKit
public struct TargetSource: Equatable {
public static let optionalDefault = false
public var path: String {
didSet {
path = (path as NSString).standardizingPath
}
}
public var name: String?
public var group: String?
public var compilerFlags: [String]
public var excludes: [String]
public var includes: [String]
public var type: SourceType?
public var optional: Bool
public var buildPhase: BuildPhaseSpec?
public var headerVisibility: HeaderVisibility?
public var createIntermediateGroups: Bool?
public var attributes: [String]
public var resourceTags: [String]
public var inferDestinationFiltersByPath: Bool?
public var destinationFilters: [SupportedDestination]?
public enum HeaderVisibility: String {
case `public`
case `private`
case project
public var settingName: String {
switch self {
case .public: return "Public"
case .private: return "Private"
case .project: return "Project"
}
}
}
public init(
path: String,
name: String? = nil,
group: String? = nil,
compilerFlags: [String] = [],
excludes: [String] = [],
includes: [String] = [],
type: SourceType? = nil,
optional: Bool = optionalDefault,
buildPhase: BuildPhaseSpec? = nil,
headerVisibility: HeaderVisibility? = nil,
createIntermediateGroups: Bool? = nil,
attributes: [String] = [],
resourceTags: [String] = [],
inferDestinationFiltersByPath: Bool? = nil,
destinationFilters: [SupportedDestination]? = nil
) {
self.path = (path as NSString).standardizingPath
self.name = name
self.group = group
self.compilerFlags = compilerFlags
self.excludes = excludes
self.includes = includes
self.type = type
self.optional = optional
self.buildPhase = buildPhase
self.headerVisibility = headerVisibility
self.createIntermediateGroups = createIntermediateGroups
self.attributes = attributes
self.resourceTags = resourceTags
self.inferDestinationFiltersByPath = inferDestinationFiltersByPath
self.destinationFilters = destinationFilters
}
}
extension TargetSource: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = TargetSource(path: value)
}
public init(extendedGraphemeClusterLiteral value: String) {
self = TargetSource(path: value)
}
public init(unicodeScalarLiteral value: String) {
self = TargetSource(path: value)
}
}
extension TargetSource: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
path = try jsonDictionary.json(atKeyPath: "path")
path = (path as NSString).standardizingPath // Done in two steps as the compiler can't figure out the types otherwise
name = jsonDictionary.json(atKeyPath: "name")
group = jsonDictionary.json(atKeyPath: "group")
let maybeCompilerFlagsString: String? = jsonDictionary.json(atKeyPath: "compilerFlags")
let maybeCompilerFlagsArray: [String]? = jsonDictionary.json(atKeyPath: "compilerFlags")
compilerFlags = maybeCompilerFlagsArray ??
maybeCompilerFlagsString.map { $0.split(separator: " ").map { String($0) } } ?? []
headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility")
excludes = jsonDictionary.json(atKeyPath: "excludes") ?? []
includes = jsonDictionary.json(atKeyPath: "includes") ?? []
type = jsonDictionary.json(atKeyPath: "type")
optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault
if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") {
buildPhase = try BuildPhaseSpec(string: string)
} else if let dict: JSONDictionary = jsonDictionary.json(atKeyPath: "buildPhase") {
buildPhase = try BuildPhaseSpec(jsonDictionary: dict)
}
createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups")
attributes = jsonDictionary.json(atKeyPath: "attributes") ?? []
resourceTags = jsonDictionary.json(atKeyPath: "resourceTags") ?? []
inferDestinationFiltersByPath = jsonDictionary.json(atKeyPath: "inferDestinationFiltersByPath")
if let destinationFilters: [SupportedDestination] = jsonDictionary.json(atKeyPath: "destinationFilters") {
self.destinationFilters = destinationFilters
}
}
}
extension TargetSource: JSONEncodable {
public func toJSONValue() -> Any {
var dict: [String: Any?] = [
"compilerFlags": compilerFlags,
"excludes": excludes,
"includes": includes,
"name": name,
"group": group,
"headerVisibility": headerVisibility?.rawValue,
"type": type?.rawValue,
"buildPhase": buildPhase?.toJSONValue(),
"createIntermediateGroups": createIntermediateGroups,
"resourceTags": resourceTags,
"path": path,
"inferDestinationFiltersByPath": inferDestinationFiltersByPath,
"destinationFilters": destinationFilters?.map { $0.rawValue },
]
if optional != TargetSource.optionalDefault {
dict["optional"] = optional
}
return dict
}
}
extension TargetSource: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("path"),
]
}
}
+76
View File
@@ -0,0 +1,76 @@
import Foundation
import JSONUtilities
struct TemplateStructure {
let baseKey: String
let templatesKey: String
let nameToReplace: String
}
extension Target {
static func resolveTargetTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary {
resolveTemplates(jsonDictionary: jsonDictionary,
templateStructure: TemplateStructure(baseKey: "targets",
templatesKey: "targetTemplates",
nameToReplace: "target_name"))
}
}
extension Scheme {
static func resolveSchemeTemplates(jsonDictionary: JSONDictionary) -> JSONDictionary {
resolveTemplates(jsonDictionary: jsonDictionary,
templateStructure: TemplateStructure(baseKey: "schemes",
templatesKey: "schemeTemplates",
nameToReplace: "scheme_name"))
}
}
private func resolveTemplates(jsonDictionary: JSONDictionary, templateStructure: TemplateStructure) -> JSONDictionary {
guard var baseDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.baseKey] as? [String: JSONDictionary] else {
return jsonDictionary
}
let templatesDictionary: [String: JSONDictionary] = jsonDictionary[templateStructure.templatesKey] as? [String: JSONDictionary] ?? [:]
// Recursively collects all nested template names of a given dictionary.
func collectTemplates(of jsonDictionary: JSONDictionary,
into allTemplates: inout [String],
insertAt insertionIndex: inout Int) {
guard let templates = jsonDictionary["templates"] as? [String] else {
return
}
for template in templates where !allTemplates.contains(template) {
guard let templateDictionary = templatesDictionary[template] else {
continue
}
allTemplates.insert(template, at: insertionIndex)
collectTemplates(of: templateDictionary, into: &allTemplates, insertAt: &insertionIndex)
insertionIndex += 1
}
}
for (referenceName, var reference) in baseDictionary {
var templates: [String] = []
var index: Int = 0
collectTemplates(of: reference, into: &templates, insertAt: &index)
if !templates.isEmpty {
var mergedDictionary: JSONDictionary = [:]
for template in templates {
if let templateDictionary = templatesDictionary[template] {
mergedDictionary = templateDictionary.merged(onto: mergedDictionary)
}
}
reference = reference.merged(onto: mergedDictionary)
reference = reference.expand(variables: [templateStructure.nameToReplace: referenceName])
if let templateAttributes = reference["templateAttributes"] as? [String: String] {
reference = reference.expand(variables: templateAttributes)
}
}
baseDictionary[referenceName] = reference
}
var jsonDictionary = jsonDictionary
jsonDictionary[templateStructure.baseKey] = baseDictionary
return jsonDictionary
}
+39
View File
@@ -0,0 +1,39 @@
import Foundation
import JSONUtilities
public struct TestPlan: Hashable {
public var path: String
public var defaultPlan: Bool
public init(path: String, defaultPlan: Bool = false) {
self.defaultPlan = defaultPlan
self.path = path
}
}
extension TestPlan: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
path = try jsonDictionary.json(atKeyPath: "path")
defaultPlan = jsonDictionary.json(atKeyPath: "defaultPlan") ?? false
}
}
extension TestPlan: JSONEncodable {
public func toJSONValue() -> Any {
[
"path": path,
"defaultPlan": defaultPlan,
] as [String : Any]
}
}
extension TestPlan: PathContainer {
static var pathProperties: [PathProperty] {
[
.string("path"),
]
}
}
@@ -0,0 +1,112 @@
import Foundation
import JSONUtilities
public struct TestableTargetReference: Hashable {
public var name: String
public var location: Location
public var targetReference: TargetReference {
switch location {
case .local:
return TargetReference(name: name, location: .local)
case .project(let projectName):
return TargetReference(name: name, location: .project(projectName))
case .package:
fatalError("Package target is only available for testable")
}
}
public enum Location: Hashable {
case local
case project(String)
case package(String)
}
public init(name: String, location: Location) {
self.name = name
self.location = location
}
}
extension TestableTargetReference {
public init(_ string: String) throws {
let paths = string.split(separator: "/")
switch paths.count {
case 2:
location = .project(String(paths[0]))
name = String(paths[1])
case 1:
location = .local
name = String(paths[0])
default:
throw SpecParsingError.invalidTargetReference(string)
}
}
public static func local(_ name: String) -> TestableTargetReference {
TestableTargetReference(name: name, location: .local)
}
public static func project(_ name: String) -> TestableTargetReference {
let paths = name.split(separator: "/")
return TestableTargetReference(name: String(paths[1]), location: .project(String(paths[0])))
}
public static func package(_ name: String) -> TestableTargetReference {
let paths = name.split(separator: "/")
return TestableTargetReference(name: String(paths[1]), location: .package(String(paths[0])))
}
}
extension TestableTargetReference: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
try! self.init(value)
}
}
extension TestableTargetReference: CustomStringConvertible {
public var reference: String {
switch location {
case .local: return name
case .project(let root), .package(let root):
return "\(root)/\(name)"
}
}
public var description: String {
reference
}
}
extension TestableTargetReference: JSONObjectConvertible {
public init(jsonDictionary: JSONDictionary) throws {
if let project: String = jsonDictionary.json(atKeyPath: "project") {
let paths = project.split(separator: "/")
name = String(paths[1])
location = .project(String(paths[0]))
} else if let project: String = jsonDictionary.json(atKeyPath: "package") {
let paths = project.split(separator: "/")
name = String(paths[1])
location = .package(String(paths[0]))
} else {
name = try jsonDictionary.json(atKeyPath: "local")
location = .local
}
}
}
extension TestableTargetReference: JSONEncodable {
public func toJSONValue() -> Any {
var dictionary: JSONDictionary = [:]
switch self.location {
case .package(let packageName):
dictionary["package"] = "\(packageName)/\(name)"
case .project(let projectName):
dictionary["project"] = "\(projectName)/\(name)"
case .local:
dictionary["local"] = name
}
return dictionary
}
}
@@ -0,0 +1,28 @@
//
// File.swift
//
//
// Created by Yonas Kolb on 7/2/20.
//
import Foundation
import Version
extension Version: Swift.ExpressibleByStringLiteral {
public static func parse(_ string: String) throws -> Version {
if let version = Version(tolerant: string) {
return version
} else {
throw SpecParsingError.invalidVersion(string)
}
}
public static func parse(_ double: Double) throws -> Version {
return try Version.parse(String(double))
}
public init(stringLiteral value: String) {
self.init(tolerant: value)!
}
}
@@ -0,0 +1,137 @@
import Foundation
import PathKit
import XcodeProj
extension PBXProductType {
init?(string: String) {
if let type = PBXProductType(rawValue: string) {
self = type
} else if let type = PBXProductType(rawValue: "com.apple.product-type.\(string)") {
self = type
} else {
return nil
}
}
public var isFramework: Bool {
self == .framework || self == .staticFramework
}
public var isLibrary: Bool {
self == .staticLibrary || self == .dynamicLibrary
}
public var isExtension: Bool {
fileExtension == "appex"
}
public var isSystemExtension: Bool {
fileExtension == "dext" || fileExtension == "systemextension"
}
public var isApp: Bool {
fileExtension == "app"
}
public var isTest: Bool {
fileExtension == "xctest"
}
public var isExecutable: Bool {
isApp || isExtension || isSystemExtension || isTest || self == .commandLineTool
}
public var name: String {
rawValue.replacingOccurrences(of: "com.apple.product-type.", with: "")
}
public var canSkipCompileSourcesBuildPhase: Bool {
switch self {
case .bundle, .watch2App, .stickerPack, .messagesApplication:
// Bundles, watch apps, sticker packs and simple messages applications without sources should not include a
// compile sources build phase. Doing so can cause Xcode to produce an error on build.
return true
default:
return false
}
}
/// Function to determine when a dependendency should be embedded into the target
public func shouldEmbed(_ dependencyTarget: Target) -> Bool {
switch dependencyTarget.defaultLinkage {
case .static:
// Static dependencies should never embed
return false
case .dynamic, .none:
if isApp {
// If target is an app, all dependencies should be embed (unless they're static)
return true
} else if isTest, [.framework, .bundle].contains(dependencyTarget.type) {
// If target is test, some dependencies should be embed (depending on their type)
return true
} else {
// If none of the above, do not embed the dependency
return false
}
}
}
}
extension Platform {
public var emoji: String {
switch self {
case .auto: return "🤖"
case .iOS: return "📱"
case .watchOS: return "⌚️"
case .tvOS: return "📺"
case .macOS: return "🖥"
case .visionOS: return "🕶️"
}
}
}
extension ProjectTarget {
public var shouldExecuteOnLaunch: Bool {
// This is different from `type.isExecutable`, because we don't want to "run" a test
type.isApp || type.isExtension || type.isSystemExtension || type == .commandLineTool
}
}
extension XCScheme.CommandLineArguments {
// Dictionary is a mapping from argument name and if it is enabled by default
public convenience init(_ dict: [String: Bool]) {
let args = dict.map { tuple in
XCScheme.CommandLineArguments.CommandLineArgument(name: tuple.key, enabled: tuple.value)
}.sorted { $0.name < $1.name }
self.init(arguments: args)
}
}
extension BreakpointExtensionID {
init(string: String) throws {
if let id = BreakpointExtensionID(rawValue: "Xcode.Breakpoint.\(string)Breakpoint") {
self = id
} else if let id = BreakpointExtensionID(rawValue: string) {
self = id
} else {
throw SpecParsingError.unknownBreakpointType(string)
}
}
}
extension BreakpointActionExtensionID {
init(string: String) throws {
if let type = BreakpointActionExtensionID(rawValue: "Xcode.BreakpointAction.\(string)") {
self = type
} else if let type = BreakpointActionExtensionID(rawValue: string) {
self = type
} else {
throw SpecParsingError.unknownBreakpointActionType(string)
}
}
}
+24
View File
@@ -0,0 +1,24 @@
import Foundation
import PathKit
import Yams
public func loadYamlDictionary(path: Path) throws -> [String: Any] {
let string: String = try path.read()
if string == "" {
return [:]
}
let resolver = Resolver.default
.removing(.null) // remove rule so that empty quotes are treated as empty strings
guard let yaml = try Yams.load(yaml: string, resolver) else {
return [:]
}
return yaml as? [String: Any] ?? [:]
}
public func dumpYamlDictionary(_ dictionary: [String: Any], path: Path) throws {
let uncluttered = (dictionary as [String: Any?]).removingEmptyArraysDictionariesAndNils()
let string: String = try Yams.dump(object: uncluttered)
try path.write(string)
}