diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index 2371f07d3..d96375f0b 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -3,168 +3,177 @@ 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 excludePatterns: [NSRegularExpression] - 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 static let optionalDefault = false - public init( - path: String, - name: String? = nil, - group: String? = nil, - compilerFlags: [String] = [], - excludes: [String] = [], - excludePatterns: [NSRegularExpression] = [], - 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.excludePatterns = excludePatterns - 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 + 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 excludePatterns: [NSRegularExpression] + 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 var brandName: String? + + 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] = [], + excludePatterns: [NSRegularExpression] = [], + 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, + brandName: String? = nil + ) { + self.path = (path as NSString).standardizingPath + self.name = name + self.group = group + self.compilerFlags = compilerFlags + self.excludes = excludes + self.excludePatterns = excludePatterns + 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 + self.brandName = brandName + } } extension TargetSource: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = TargetSource(path: value) - } + public init(stringLiteral value: String) { + self = TargetSource(path: value) + } - public init(extendedGraphemeClusterLiteral value: String) { - self = TargetSource(path: value) - } + public init(extendedGraphemeClusterLiteral value: String) { + self = TargetSource(path: value) + } - public init(unicodeScalarLiteral 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") ?? [] - let regexPatterns: [String] = jsonDictionary.json(atKeyPath: "excludePatterns") ?? [] - excludePatterns = try regexPatterns.map({ - try NSRegularExpression(pattern: $0) - }) - 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 - } + 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") ?? [] + let regexPatterns: [String] = jsonDictionary.json(atKeyPath: "excludePatterns") ?? [] + excludePatterns = try regexPatterns.map({ + try NSRegularExpression(pattern: $0) + }) + 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 + } + + brandName = jsonDictionary.json(atKeyPath: "brandName") + } } 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 + 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"), - ] - } + static var pathProperties: [PathProperty] { + [ + .string("path") + ] + } } diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 445b7d36c..aac8b771c 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -1,1665 +1,1818 @@ import Foundation import PathKit import ProjectSpec +import Version import XcodeProj import Yams -import Version public class PBXProjGenerator { - let project: Project - - let pbxProj: PBXProj - let projectDirectory: Path? - let carthageResolver: CarthageDependencyResolver + let project: Project - public static let copyFilesActionMask: UInt = 8 + let pbxProj: PBXProj + let projectDirectory: Path? + let carthageResolver: CarthageDependencyResolver - var sourceGenerator: SourceGenerator! + public static let copyFilesActionMask: UInt = 8 - var targetObjects: [String: PBXTarget] = [:] - var targetAggregateObjects: [String: PBXAggregateTarget] = [:] - var targetFileReferences: [String: PBXFileReference] = [:] - var sdkFileReferences: [String: PBXFileReference] = [:] - var packageReferences: [String: XCRemoteSwiftPackageReference] = [:] - var localPackageReferences: [String: XCLocalSwiftPackageReference] = [:] + var sourceGenerator: SourceGenerator! - var carthageFrameworksByPlatform: [String: Set] = [:] - var frameworkFiles: [PBXFileElement] = [] - var bundleFiles: [PBXFileElement] = [] + var targetObjects: [String: PBXTarget] = [:] + var targetAggregateObjects: [String: PBXAggregateTarget] = [:] + var targetFileReferences: [String: PBXFileReference] = [:] + var sdkFileReferences: [String: PBXFileReference] = [:] + var packageReferences: [String: XCRemoteSwiftPackageReference] = [:] + var localPackageReferences: [String: XCLocalSwiftPackageReference] = [:] - var generated = false + var carthageFrameworksByPlatform: [String: Set] = [:] + var frameworkFiles: [PBXFileElement] = [] + var bundleFiles: [PBXFileElement] = [] - private var projects: [ProjectReference: PBXProj] = [:] + var generated = false - public init(project: Project, projectDirectory: Path? = nil) { - self.project = project - carthageResolver = CarthageDependencyResolver(project: project) - pbxProj = PBXProj(rootObject: nil, objectVersion: project.objectVersion) - self.projectDirectory = projectDirectory - sourceGenerator = SourceGenerator(project: project, - pbxProj: pbxProj, - projectDirectory: projectDirectory) - } + private var projects: [ProjectReference: PBXProj] = [:] - @discardableResult - func addObject(_ object: T, context: String? = nil) -> T { - pbxProj.add(object: object) - object.context = context - return object - } + public init(project: Project, projectDirectory: Path? = nil) { + self.project = project + carthageResolver = CarthageDependencyResolver(project: project) + pbxProj = PBXProj(rootObject: nil, objectVersion: project.objectVersion) + self.projectDirectory = projectDirectory + sourceGenerator = SourceGenerator( + project: project, + pbxProj: pbxProj, + projectDirectory: projectDirectory) + } - public func generate() throws -> PBXProj { - if generated { - fatalError("Cannot use PBXProjGenerator to generate more than once") - } - generated = true + @discardableResult + func addObject(_ object: T, context: String? = nil) -> T { + pbxProj.add(object: object) + object.context = context + return object + } - for group in project.fileGroups { - try sourceGenerator.getFileGroups(path: group) - } + public func generate() throws -> PBXProj { + if generated { + fatalError("Cannot use PBXProjGenerator to generate more than once") + } + generated = true - let buildConfigs: [XCBuildConfiguration] = project.configs.map { config in - let buildSettings = project.getProjectBuildSettings(config: config) - var baseConfiguration: PBXFileReference? - if let configPath = project.configFiles[config.name], - let fileReference = sourceGenerator.getContainedFileReference(path: project.basePath + configPath) as? PBXFileReference { - baseConfiguration = fileReference - } - let buildConfig = addObject( - XCBuildConfiguration( - name: config.name, - buildSettings: buildSettings - ) - ) - buildConfig.baseConfiguration = baseConfiguration - return buildConfig - } + for group in project.fileGroups { + try sourceGenerator.getFileGroups(path: group) + } - let configName = project.options.defaultConfig ?? buildConfigs.first?.name ?? "" - let buildConfigList = addObject( - XCConfigurationList( - buildConfigurations: buildConfigs, - defaultConfigurationName: configName - ) + let buildConfigs: [XCBuildConfiguration] = project.configs.map { config in + let buildSettings = project.getProjectBuildSettings(config: config) + var baseConfiguration: PBXFileReference? + if let configPath = project.configFiles[config.name], + let fileReference = sourceGenerator.getContainedFileReference( + path: project.basePath + configPath) as? PBXFileReference + { + baseConfiguration = fileReference + } + let buildConfig = addObject( + XCBuildConfiguration( + name: config.name, + buildSettings: buildSettings ) + ) + buildConfig.baseConfiguration = baseConfiguration + return buildConfig + } - var derivedGroups: [PBXGroup] = [] - - let mainGroup = addObject( - PBXGroup( - children: [], - sourceTree: .group, - usesTabs: project.options.usesTabs, - indentWidth: project.options.indentWidth, - tabWidth: project.options.tabWidth - ) + let configName = project.options.defaultConfig ?? buildConfigs.first?.name ?? "" + let buildConfigList = addObject( + XCConfigurationList( + buildConfigurations: buildConfigs, + defaultConfigurationName: configName + ) + ) + + var derivedGroups: [PBXGroup] = [] + + let mainGroup = addObject( + PBXGroup( + children: [], + sourceTree: .group, + usesTabs: project.options.usesTabs, + indentWidth: project.options.indentWidth, + tabWidth: project.options.tabWidth + ) + ) + + let developmentRegion = project.options.developmentLanguage ?? "en" + let pbxProject = addObject( + PBXProject( + name: project.name, + buildConfigurationList: buildConfigList, + compatibilityVersion: project.compatibilityVersion, + preferredProjectObjectVersion: Int(project.objectVersion), + minimizedProjectReferenceProxies: project.minimizedProjectReferenceProxies, + mainGroup: mainGroup, + developmentRegion: developmentRegion + ) + ) + + pbxProj.rootObject = pbxProject + + for target in project.targets { + let targetObject: PBXTarget + + if target.isLegacy { + targetObject = PBXLegacyTarget( + name: target.name, + buildToolPath: target.legacy?.toolPath, + buildArgumentsString: target.legacy?.arguments, + passBuildSettingsInEnvironment: target.legacy?.passSettings ?? false, + buildWorkingDirectory: target.legacy?.workingDirectory, + buildPhases: [] ) - - let developmentRegion = project.options.developmentLanguage ?? "en" - let pbxProject = addObject( - PBXProject( - name: project.name, - buildConfigurationList: buildConfigList, - compatibilityVersion: project.compatibilityVersion, - preferredProjectObjectVersion: Int(project.objectVersion), - minimizedProjectReferenceProxies: project.minimizedProjectReferenceProxies, - mainGroup: mainGroup, - developmentRegion: developmentRegion - ) + } else { + targetObject = PBXNativeTarget(name: target.name, buildPhases: []) + } + + targetObjects[target.name] = addObject(targetObject) + + var explicitFileType: String? + var lastKnownFileType: String? + let fileType = Xcode.fileType(path: Path(target.filename), productType: target.type) + if target.platform == .macOS || target.platform == .watchOS || target.type == .framework + || target.type == .extensionKitExtension + { + explicitFileType = fileType + } else { + lastKnownFileType = fileType + } + + if !target.isLegacy { + let fileReference = addObject( + PBXFileReference( + sourceTree: .buildProductsDir, + explicitFileType: explicitFileType, + lastKnownFileType: lastKnownFileType, + path: target.filename, + includeInIndex: false + ), + context: target.name ) - pbxProj.rootObject = pbxProject - - for target in project.targets { - let targetObject: PBXTarget - - if target.isLegacy { - targetObject = PBXLegacyTarget( - name: target.name, - buildToolPath: target.legacy?.toolPath, - buildArgumentsString: target.legacy?.arguments, - passBuildSettingsInEnvironment: target.legacy?.passSettings ?? false, - buildWorkingDirectory: target.legacy?.workingDirectory, - buildPhases: [] - ) - } else { - targetObject = PBXNativeTarget(name: target.name, buildPhases: []) - } - - targetObjects[target.name] = addObject(targetObject) - - var explicitFileType: String? - var lastKnownFileType: String? - let fileType = Xcode.fileType(path: Path(target.filename), productType: target.type) - if target.platform == .macOS || target.platform == .watchOS || target.type == .framework || target.type == .extensionKitExtension { - explicitFileType = fileType - } else { - lastKnownFileType = fileType - } - - if !target.isLegacy { - let fileReference = addObject( - PBXFileReference( - sourceTree: .buildProductsDir, - explicitFileType: explicitFileType, - lastKnownFileType: lastKnownFileType, - path: target.filename, - includeInIndex: false - ), - context: target.name - ) - - targetFileReferences[target.name] = fileReference - } - } - - for target in project.aggregateTargets { - - let aggregateTarget = addObject( - PBXAggregateTarget( - name: target.name, - productName: target.name - ) - ) - targetAggregateObjects[target.name] = aggregateTarget - } + targetFileReferences[target.name] = fileReference + } + } - for (name, package) in project.packages { - switch package { - case let .remote(url, versionRequirement): - let packageReference = XCRemoteSwiftPackageReference(repositoryURL: url, versionRequirement: versionRequirement) - packageReferences[name] = packageReference - addObject(packageReference) - case let .local(path, group, excludeFromProject): - let packageReference = XCLocalSwiftPackageReference(relativePath: path) - localPackageReferences[name] = packageReference - - if !excludeFromProject { - addObject(packageReference) - try sourceGenerator.createLocalPackage(path: Path(path), group: group.map { Path($0) }) - } - } - } + for target in project.aggregateTargets { - let productGroup = addObject( - PBXGroup( - children: targetFileReferences.valueArray, - sourceTree: .group, - name: "Products" - ) + let aggregateTarget = addObject( + PBXAggregateTarget( + name: target.name, + productName: target.name ) - derivedGroups.append(productGroup) - - let sortedProjectReferences = project.projectReferences.sorted { $0.name < $1.name } - let subprojectFileReferences: [PBXFileReference] = sortedProjectReferences.map { projectReference in - let projectPath = Path(projectReference.path) - - return addObject( - PBXFileReference( - sourceTree: .group, - name: projectReference.name, - lastKnownFileType: Xcode.fileType(path: projectPath), - path: projectPath.normalize().string - ) - ) - } - if subprojectFileReferences.count > 0 { - let subprojectsGroups = addObject( - PBXGroup( - children: subprojectFileReferences, - sourceTree: .group, - name: "Projects" - ) - ) - derivedGroups.append(subprojectsGroups) - - let subprojects: [[String: PBXFileElement]] = subprojectFileReferences.map { projectReference in - let group = addObject( - PBXGroup( - children: [], - sourceTree: .group, - name: "Products" - ) - ) - return [ - "ProductGroup": group, - "ProjectRef": projectReference, - ] - } - - pbxProject.projects = subprojects - } - - try project.targets.forEach(generateTarget) - try project.aggregateTargets.forEach(generateAggregateTarget) - - if !carthageFrameworksByPlatform.isEmpty { - var platforms: [PBXGroup] = [] - for (platform, files) in carthageFrameworksByPlatform { - let platformGroup: PBXGroup = addObject( - PBXGroup( - children: Array(files), - sourceTree: .group, - path: platform - ) - ) - platforms.append(platformGroup) - } - let carthageGroup = addObject( - PBXGroup( - children: platforms, - sourceTree: .group, - name: "Carthage", - path: carthageResolver.buildPath - ) - ) - frameworkFiles.append(carthageGroup) - } - - if !frameworkFiles.isEmpty { - let group = addObject( - PBXGroup( - children: frameworkFiles, - sourceTree: .group, - name: "Frameworks" - ) - ) - derivedGroups.append(group) - } - - if !bundleFiles.isEmpty { - let group = addObject( - PBXGroup( - children: bundleFiles, - sourceTree: .group, - name: "Bundles" - ) - ) - derivedGroups.append(group) - } - - mainGroup.children = Array(sourceGenerator.rootGroups) - sortGroups(group: mainGroup) - setupGroupOrdering(group: mainGroup) - // add derived groups at the end - derivedGroups.forEach(sortGroups) - mainGroup.children += derivedGroups - .sorted(by: PBXFileElement.sortByNamePath) - .map { $0 } - - let assetTags = Set(project.targets - .map { target in - target.sources.map { $0.resourceTags }.flatMap { $0 } - }.flatMap { $0 } - ).sorted() - - let defaultAttributes: [String: Any] = [ - "BuildIndependentTargetsInParallel": "YES" - ] - var projectAttributes: [String: Any] = defaultAttributes.merged(project.attributes) - - // Set default LastUpgradeCheck if user did not specify - let lastUpgradeKey = "LastUpgradeCheck" - if !projectAttributes.contains(where: { (key, value) -> Bool in - key == lastUpgradeKey && value is String - }) { - projectAttributes[lastUpgradeKey] = project.xcodeVersion - } - - if !assetTags.isEmpty { - projectAttributes["knownAssetTags"] = assetTags - } - - var knownRegions = Set(sourceGenerator.knownRegions) - knownRegions.insert(developmentRegion) - if project.options.useBaseInternationalization { - knownRegions.insert("Base") - } - pbxProject.knownRegions = knownRegions.sorted() - - pbxProject.remotePackages = packageReferences.sorted { $0.key < $1.key }.map { $1 } - pbxProject.localPackages = localPackageReferences.sorted { $0.key < $1.key }.map { $1 } - - let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray - pbxProject.targets = allTargets - .sorted { $0.name < $1.name } - pbxProject.attributes = projectAttributes - pbxProject.targetAttributes = generateTargetAttributes() - return pbxProj + ) + targetAggregateObjects[target.name] = aggregateTarget } - func generateAggregateTarget(_ target: AggregateTarget) throws { - - let aggregateTarget = targetAggregateObjects[target.name]! - - let configs: [XCBuildConfiguration] = project.configs.map { config in - - let buildSettings = project.getBuildSettings(settings: target.settings, config: config) - - var baseConfiguration: PBXFileReference? - if let configPath = target.configFiles[config.name] { - baseConfiguration = sourceGenerator.getContainedFileReference(path: project.basePath + configPath) as? PBXFileReference - } - let buildConfig = XCBuildConfiguration( - name: config.name, - baseConfiguration: baseConfiguration, - buildSettings: buildSettings - ) - return addObject(buildConfig) - } - - var dependencies = target.targets.map { generateTargetDependency(from: target.name, to: $0, platform: nil, platforms: nil) } - - let defaultConfigurationName = project.options.defaultConfig ?? project.configs.first?.name ?? "" - let buildConfigList = addObject(XCConfigurationList( - buildConfigurations: configs, - defaultConfigurationName: defaultConfigurationName - )) - - var buildPhases: [PBXBuildPhase] = [] - buildPhases += try target.buildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) } - - let packagePluginDependencies = makePackagePluginDependency(for: target) - dependencies.append(contentsOf: packagePluginDependencies) + for (name, package) in project.packages { + switch package { + case let .remote(url, versionRequirement): + let packageReference = XCRemoteSwiftPackageReference( + repositoryURL: url, versionRequirement: versionRequirement) + packageReferences[name] = packageReference + addObject(packageReference) + case let .local(path, group, excludeFromProject): + let packageReference = XCLocalSwiftPackageReference(relativePath: path) + localPackageReferences[name] = packageReference + + if !excludeFromProject { + addObject(packageReference) + try sourceGenerator.createLocalPackage(path: Path(path), group: group.map { Path($0) }) + } + } + } - aggregateTarget.buildPhases = buildPhases - aggregateTarget.buildConfigurationList = buildConfigList - aggregateTarget.dependencies = dependencies + let productGroup = addObject( + PBXGroup( + children: targetFileReferences.valueArray, + sourceTree: .group, + name: "Products" + ) + ) + derivedGroups.append(productGroup) + + let sortedProjectReferences = project.projectReferences.sorted { $0.name < $1.name } + let subprojectFileReferences: [PBXFileReference] = sortedProjectReferences.map { + projectReference in + let projectPath = Path(projectReference.path) + + return addObject( + PBXFileReference( + sourceTree: .group, + name: projectReference.name, + lastKnownFileType: Xcode.fileType(path: projectPath), + path: projectPath.normalize().string + ) + ) } + if subprojectFileReferences.count > 0 { + let subprojectsGroups = addObject( + PBXGroup( + children: subprojectFileReferences, + sourceTree: .group, + name: "Projects" + ) + ) + derivedGroups.append(subprojectsGroups) + + let subprojects: [[String: PBXFileElement]] = subprojectFileReferences.map { + projectReference in + let group = addObject( + PBXGroup( + children: [], + sourceTree: .group, + name: "Products" + ) + ) + return [ + "ProductGroup": group, + "ProjectRef": projectReference, + ] + } - func generateTargetDependency(from: String, to target: String, platform: String?, platforms: [String]?) -> PBXTargetDependency { - guard let targetObject = targetObjects[target] ?? targetAggregateObjects[target] else { - fatalError("Target dependency not found: from ( \(from) ) to ( \(target) )") - } + pbxProject.projects = subprojects + } - let targetProxy = addObject( - PBXContainerItemProxy( - containerPortal: .project(pbxProj.rootObject!), - remoteGlobalID: .object(targetObject), - proxyType: .nativeTarget, - remoteInfo: target - ) + try project.targets.forEach(generateTarget) + try project.aggregateTargets.forEach(generateAggregateTarget) + + if !carthageFrameworksByPlatform.isEmpty { + var platforms: [PBXGroup] = [] + for (platform, files) in carthageFrameworksByPlatform { + let platformGroup: PBXGroup = addObject( + PBXGroup( + children: Array(files), + sourceTree: .group, + path: platform + ) ) - - let targetDependency = addObject( - PBXTargetDependency( - platformFilter: platform, - platformFilters: platforms, - target: targetObject, - targetProxy: targetProxy - ) + platforms.append(platformGroup) + } + let carthageGroup = addObject( + PBXGroup( + children: platforms, + sourceTree: .group, + name: "Carthage", + path: carthageResolver.buildPath ) - return targetDependency + ) + frameworkFiles.append(carthageGroup) } - func generateExternalTargetDependency(from: String, to target: String, in project: String, platform: Platform) throws -> (PBXTargetDependency, Target, PBXReferenceProxy) { - guard let projectReference = self.project.getProjectReference(project) else { - fatalError("project '\(project)' not found") - } - - let pbxProj = try getPBXProj(from: projectReference) - - guard let targetObject = pbxProj.targets(named: target).first else { - fatalError("target '\(target)' not found in project '\(project)'") - } - - let projectFileReferenceIndex = self.pbxProj.rootObject! - .projects - .map { $0["ProjectRef"] as? PBXFileReference } - .firstIndex { $0?.path == Path(projectReference.path).normalize().string } - - guard let index = projectFileReferenceIndex, - let projectFileReference = self.pbxProj.rootObject?.projects[index]["ProjectRef"] as? PBXFileReference, - let productsGroup = self.pbxProj.rootObject?.projects[index]["ProductGroup"] as? PBXGroup else { - fatalError("Missing subproject file reference") - } - - let targetProxy = addObject( - PBXContainerItemProxy( - containerPortal: .fileReference(projectFileReference), - remoteGlobalID: .object(targetObject), - proxyType: .nativeTarget, - remoteInfo: target - ) + if !frameworkFiles.isEmpty { + let group = addObject( + PBXGroup( + children: frameworkFiles, + sourceTree: .group, + name: "Frameworks" ) + ) + derivedGroups.append(group) + } - let productProxy = PBXContainerItemProxy( - containerPortal: .fileReference(projectFileReference), - remoteGlobalID: targetObject.product.flatMap(PBXContainerItemProxy.RemoteGlobalID.object), - proxyType: .reference, - remoteInfo: target + if !bundleFiles.isEmpty { + let group = addObject( + PBXGroup( + children: bundleFiles, + sourceTree: .group, + name: "Bundles" ) + ) + derivedGroups.append(group) + } - var path = targetObject.productNameWithExtension() + mainGroup.children = Array(sourceGenerator.rootGroups) + sortGroups(group: mainGroup) + setupGroupOrdering(group: mainGroup) + // add derived groups at the end + derivedGroups.forEach(sortGroups) + mainGroup.children += + derivedGroups + .sorted(by: PBXFileElement.sortByNamePath) + .map { $0 } + + let assetTags = Set( + project.targets + .map { target in + target.sources.map { $0.resourceTags }.flatMap { $0 } + }.flatMap { $0 } + ).sorted() + + let defaultAttributes: [String: Any] = [ + "BuildIndependentTargetsInParallel": "YES" + ] + var projectAttributes: [String: Any] = defaultAttributes.merged(project.attributes) + + // Set default LastUpgradeCheck if user did not specify + let lastUpgradeKey = "LastUpgradeCheck" + if !projectAttributes.contains(where: { (key, value) -> Bool in + key == lastUpgradeKey && value is String + }) { + projectAttributes[lastUpgradeKey] = project.xcodeVersion + } - if targetObject.productType == .staticLibrary, - let tmpPath = path, !tmpPath.hasPrefix("lib") { - path = "lib\(tmpPath)" - } + if !assetTags.isEmpty { + projectAttributes["knownAssetTags"] = assetTags + } - let productReferenceProxyFileType = targetObject.productNameWithExtension() - .flatMap { Xcode.fileType(path: Path($0)) } + var knownRegions = Set(sourceGenerator.knownRegions) + knownRegions.insert(developmentRegion) + if project.options.useBaseInternationalization { + knownRegions.insert("Base") + } + pbxProject.knownRegions = knownRegions.sorted() + + pbxProject.remotePackages = packageReferences.sorted { $0.key < $1.key }.map { $1 } + pbxProject.localPackages = localPackageReferences.sorted { $0.key < $1.key }.map { $1 } + + let allTargets: [PBXTarget] = targetObjects.valueArray + targetAggregateObjects.valueArray + pbxProject.targets = + allTargets + .sorted { $0.name < $1.name } + pbxProject.attributes = projectAttributes + pbxProject.targetAttributes = generateTargetAttributes() + return pbxProj + } + + func generateAggregateTarget(_ target: AggregateTarget) throws { + + let aggregateTarget = targetAggregateObjects[target.name]! + + let configs: [XCBuildConfiguration] = project.configs.map { config in + + let buildSettings = project.getBuildSettings(settings: target.settings, config: config) + + var baseConfiguration: PBXFileReference? + if let configPath = target.configFiles[config.name] { + baseConfiguration = + sourceGenerator.getContainedFileReference(path: project.basePath + configPath) + as? PBXFileReference + } + let buildConfig = XCBuildConfiguration( + name: config.name, + baseConfiguration: baseConfiguration, + buildSettings: buildSettings + ) + return addObject(buildConfig) + } - let existingValue = self.pbxProj.referenceProxies.first { referenceProxy in - referenceProxy.path == path && - referenceProxy.remote == productProxy && - referenceProxy.sourceTree == .buildProductsDir && - referenceProxy.fileType == productReferenceProxyFileType - } + var dependencies = target.targets.map { + generateTargetDependency(from: target.name, to: $0, platform: nil, platforms: nil) + } - let productReferenceProxy: PBXReferenceProxy - if let existingValue = existingValue { - productReferenceProxy = existingValue - } else { - addObject(productProxy) - productReferenceProxy = addObject( - PBXReferenceProxy( - fileType: productReferenceProxyFileType, - path: path, - remote: productProxy, - sourceTree: .buildProductsDir - ) - ) + let defaultConfigurationName = + project.options.defaultConfig ?? project.configs.first?.name ?? "" + let buildConfigList = addObject( + XCConfigurationList( + buildConfigurations: configs, + defaultConfigurationName: defaultConfigurationName + )) + + var buildPhases: [PBXBuildPhase] = [] + buildPhases += try target.buildScripts.map { + try generateBuildScript(targetName: target.name, buildScript: $0) + } - productsGroup.children.append(productReferenceProxy) - } + let packagePluginDependencies = makePackagePluginDependency(for: target) + dependencies.append(contentsOf: packagePluginDependencies) + aggregateTarget.buildPhases = buildPhases + aggregateTarget.buildConfigurationList = buildConfigList + aggregateTarget.dependencies = dependencies + } - let targetDependency = addObject( - PBXTargetDependency( - name: targetObject.name, - targetProxy: targetProxy - ) - ) + func generateTargetDependency( + from: String, to target: String, platform: String?, platforms: [String]? + ) -> PBXTargetDependency { + guard let targetObject = targetObjects[target] ?? targetAggregateObjects[target] else { + fatalError("Target dependency not found: from ( \(from) ) to ( \(target) )") + } - guard let buildConfigurations = targetObject.buildConfigurationList?.buildConfigurations, - let defaultConfigurationName = targetObject.buildConfigurationList?.defaultConfigurationName, - let defaultConfiguration = buildConfigurations.first(where: { $0.name == defaultConfigurationName }) ?? buildConfigurations.first else { + let targetProxy = addObject( + PBXContainerItemProxy( + containerPortal: .project(pbxProj.rootObject!), + remoteGlobalID: .object(targetObject), + proxyType: .nativeTarget, + remoteInfo: target + ) + ) + + let targetDependency = addObject( + PBXTargetDependency( + platformFilter: platform, + platformFilters: platforms, + target: targetObject, + targetProxy: targetProxy + ) + ) + return targetDependency + } + + func generateExternalTargetDependency( + from: String, to target: String, in project: String, platform: Platform + ) throws -> (PBXTargetDependency, Target, PBXReferenceProxy) { + guard let projectReference = self.project.getProjectReference(project) else { + fatalError("project '\(project)' not found") + } - fatalError("Missing target info") - } + let pbxProj = try getPBXProj(from: projectReference) - let productType: PBXProductType = targetObject.productType ?? .none - let buildSettings = defaultConfiguration.buildSettings - let settings = Settings(buildSettings: buildSettings, configSettings: [:], groups: []) - let deploymentTargetString = buildSettings[platform.deploymentTargetSetting] as? String - let deploymentTarget = deploymentTargetString == nil ? nil : try Version.parse(deploymentTargetString!) - let requiresObjCLinking = (buildSettings["OTHER_LDFLAGS"] as? String)?.contains("-ObjC") ?? (productType == .staticLibrary) - let dependencyTarget = Target( - name: targetObject.name, - type: productType, - platform: platform, - productName: targetObject.productName, - deploymentTarget: deploymentTarget, - settings: settings, - requiresObjCLinking: requiresObjCLinking - ) + guard let targetObject = pbxProj.targets(named: target).first else { + fatalError("target '\(target)' not found in project '\(project)'") + } - return (targetDependency, dependencyTarget, productReferenceProxy) + let projectFileReferenceIndex = self.pbxProj.rootObject! + .projects + .map { $0["ProjectRef"] as? PBXFileReference } + .firstIndex { $0?.path == Path(projectReference.path).normalize().string } + + guard let index = projectFileReferenceIndex, + let projectFileReference = self.pbxProj.rootObject?.projects[index]["ProjectRef"] + as? PBXFileReference, + let productsGroup = self.pbxProj.rootObject?.projects[index]["ProductGroup"] as? PBXGroup + else { + fatalError("Missing subproject file reference") } - func generateBuildScript(targetName: String, buildScript: BuildScript) throws -> PBXShellScriptBuildPhase { + let targetProxy = addObject( + PBXContainerItemProxy( + containerPortal: .fileReference(projectFileReference), + remoteGlobalID: .object(targetObject), + proxyType: .nativeTarget, + remoteInfo: target + ) + ) + + let productProxy = PBXContainerItemProxy( + containerPortal: .fileReference(projectFileReference), + remoteGlobalID: targetObject.product.flatMap(PBXContainerItemProxy.RemoteGlobalID.object), + proxyType: .reference, + remoteInfo: target + ) + + var path = targetObject.productNameWithExtension() + + if targetObject.productType == .staticLibrary, + let tmpPath = path, !tmpPath.hasPrefix("lib") + { + path = "lib\(tmpPath)" + } - let shellScript: String - switch buildScript.script { - case let .path(path): - shellScript = try (project.basePath + path).read() - case let .script(script): - shellScript = script - } + let productReferenceProxyFileType = targetObject.productNameWithExtension() + .flatMap { Xcode.fileType(path: Path($0)) } - let shellScriptPhase = PBXShellScriptBuildPhase( - name: buildScript.name ?? "Run Script", - inputPaths: buildScript.inputFiles, - outputPaths: buildScript.outputFiles, - inputFileListPaths: buildScript.inputFileLists, - outputFileListPaths: buildScript.outputFileLists, - shellPath: buildScript.shell ?? "/bin/sh", - shellScript: shellScript, - runOnlyForDeploymentPostprocessing: buildScript.runOnlyWhenInstalling, - showEnvVarsInLog: buildScript.showEnvVars, - alwaysOutOfDate: !buildScript.basedOnDependencyAnalysis, - dependencyFile: buildScript.discoveredDependencyFile - ) - return addObject(shellScriptPhase) + let existingValue = self.pbxProj.referenceProxies.first { referenceProxy in + referenceProxy.path == path && referenceProxy.remote == productProxy + && referenceProxy.sourceTree == .buildProductsDir + && referenceProxy.fileType == productReferenceProxyFileType } - func generateCopyFiles(targetName: String, copyFiles: BuildPhaseSpec.CopyFilesSettings, buildPhaseFiles: [PBXBuildFile]) -> PBXCopyFilesBuildPhase { - let copyFilesBuildPhase = PBXCopyFilesBuildPhase( - dstPath: copyFiles.subpath, - dstSubfolderSpec: copyFiles.destination.destination, - files: buildPhaseFiles + let productReferenceProxy: PBXReferenceProxy + if let existingValue = existingValue { + productReferenceProxy = existingValue + } else { + addObject(productProxy) + productReferenceProxy = addObject( + PBXReferenceProxy( + fileType: productReferenceProxyFileType, + path: path, + remote: productProxy, + sourceTree: .buildProductsDir ) - return addObject(copyFilesBuildPhase) - } - - func generateTargetAttributes() -> [PBXTarget: [String: Any]] { + ) - var targetAttributes: [PBXTarget: [String: Any]] = [:] - - let testTargets = pbxProj.nativeTargets.filter { $0.productType == .uiTestBundle || $0.productType == .unitTestBundle } - for testTarget in testTargets { + productsGroup.children.append(productReferenceProxy) + } - // look up TEST_TARGET_NAME build setting - func testTargetName(_ target: PBXTarget) -> String? { - guard let buildConfigurations = target.buildConfigurationList?.buildConfigurations else { return nil } + let targetDependency = addObject( + PBXTargetDependency( + name: targetObject.name, + targetProxy: targetProxy + ) + ) + + guard let buildConfigurations = targetObject.buildConfigurationList?.buildConfigurations, + let defaultConfigurationName = targetObject.buildConfigurationList?.defaultConfigurationName, + let defaultConfiguration = buildConfigurations.first(where: { + $0.name == defaultConfigurationName + }) ?? buildConfigurations.first + else { + + fatalError("Missing target info") + } - return buildConfigurations - .compactMap { $0.buildSettings["TEST_TARGET_NAME"] as? String } - .first - } + let productType: PBXProductType = targetObject.productType ?? .none + let buildSettings = defaultConfiguration.buildSettings + let settings = Settings(buildSettings: buildSettings, configSettings: [:], groups: []) + let deploymentTargetString = buildSettings[platform.deploymentTargetSetting] as? String + let deploymentTarget = + deploymentTargetString == nil ? nil : try Version.parse(deploymentTargetString!) + let requiresObjCLinking = + (buildSettings["OTHER_LDFLAGS"] as? String)?.contains("-ObjC") + ?? (productType == .staticLibrary) + let dependencyTarget = Target( + name: targetObject.name, + type: productType, + platform: platform, + productName: targetObject.productName, + deploymentTarget: deploymentTarget, + settings: settings, + requiresObjCLinking: requiresObjCLinking + ) + + return (targetDependency, dependencyTarget, productReferenceProxy) + } + + func generateBuildScript(targetName: String, buildScript: BuildScript) throws + -> PBXShellScriptBuildPhase + { + + let shellScript: String + switch buildScript.script { + case let .path(path): + shellScript = try (project.basePath + path).read() + case let .script(script): + shellScript = script + } - guard let name = testTargetName(testTarget) else { continue } - guard let target = self.pbxProj.targets(named: name).first else { continue } + let shellScriptPhase = PBXShellScriptBuildPhase( + name: buildScript.name ?? "Run Script", + inputPaths: buildScript.inputFiles, + outputPaths: buildScript.outputFiles, + inputFileListPaths: buildScript.inputFileLists, + outputFileListPaths: buildScript.outputFileLists, + shellPath: buildScript.shell ?? "/bin/sh", + shellScript: shellScript, + runOnlyForDeploymentPostprocessing: buildScript.runOnlyWhenInstalling, + showEnvVarsInLog: buildScript.showEnvVars, + alwaysOutOfDate: !buildScript.basedOnDependencyAnalysis, + dependencyFile: buildScript.discoveredDependencyFile + ) + return addObject(shellScriptPhase) + } + + func generateCopyFiles( + targetName: String, copyFiles: BuildPhaseSpec.CopyFilesSettings, buildPhaseFiles: [PBXBuildFile] + ) -> PBXCopyFilesBuildPhase { + let copyFilesBuildPhase = PBXCopyFilesBuildPhase( + dstPath: copyFiles.subpath, + dstSubfolderSpec: copyFiles.destination.destination, + files: buildPhaseFiles + ) + return addObject(copyFilesBuildPhase) + } + + func generateTargetAttributes() -> [PBXTarget: [String: Any]] { + + var targetAttributes: [PBXTarget: [String: Any]] = [:] + + let testTargets = pbxProj.nativeTargets.filter { + $0.productType == .uiTestBundle || $0.productType == .unitTestBundle + } + for testTarget in testTargets { - targetAttributes[testTarget, default: [:]].merge(["TestTargetID": target]) + // look up TEST_TARGET_NAME build setting + func testTargetName(_ target: PBXTarget) -> String? { + guard let buildConfigurations = target.buildConfigurationList?.buildConfigurations else { + return nil } - func generateTargetAttributes(_ target: ProjectTarget, pbxTarget: PBXTarget) { - if !target.attributes.isEmpty { - targetAttributes[pbxTarget, default: [:]].merge(target.attributes) - } + return + buildConfigurations + .compactMap { $0.buildSettings["TEST_TARGET_NAME"] as? String } + .first + } - func getSingleBuildSetting(_ setting: String) -> String? { - let settings = project.configs.compactMap { - project.getCombinedBuildSetting(setting, target: target, config: $0) as? String - } - guard settings.count == project.configs.count, - let firstSetting = settings.first, - settings.filter({ $0 == firstSetting }).count == settings.count else { - return nil - } - return firstSetting - } + guard let name = testTargetName(testTarget) else { continue } + guard let target = self.pbxProj.targets(named: name).first else { continue } - func setTargetAttribute(attribute: String, buildSetting: String) { - if let setting = getSingleBuildSetting(buildSetting) { - targetAttributes[pbxTarget, default: [:]].merge([attribute: setting]) - } - } + targetAttributes[testTarget, default: [:]].merge(["TestTargetID": target]) + } - setTargetAttribute(attribute: "ProvisioningStyle", buildSetting: "CODE_SIGN_STYLE") - setTargetAttribute(attribute: "DevelopmentTeam", buildSetting: "DEVELOPMENT_TEAM") - } + func generateTargetAttributes(_ target: ProjectTarget, pbxTarget: PBXTarget) { + if !target.attributes.isEmpty { + targetAttributes[pbxTarget, default: [:]].merge(target.attributes) + } - for target in project.aggregateTargets { - guard let pbxTarget = targetAggregateObjects[target.name] else { - continue - } - generateTargetAttributes(target, pbxTarget: pbxTarget) + func getSingleBuildSetting(_ setting: String) -> String? { + let settings = project.configs.compactMap { + project.getCombinedBuildSetting(setting, target: target, config: $0) as? String + } + guard settings.count == project.configs.count, + let firstSetting = settings.first, + settings.filter({ $0 == firstSetting }).count == settings.count + else { + return nil } + return firstSetting + } - for target in project.targets { - guard let pbxTarget = targetObjects[target.name] else { - continue - } - generateTargetAttributes(target, pbxTarget: pbxTarget) + func setTargetAttribute(attribute: String, buildSetting: String) { + if let setting = getSingleBuildSetting(buildSetting) { + targetAttributes[pbxTarget, default: [:]].merge([attribute: setting]) } + } - return targetAttributes + setTargetAttribute(attribute: "ProvisioningStyle", buildSetting: "CODE_SIGN_STYLE") + setTargetAttribute(attribute: "DevelopmentTeam", buildSetting: "DEVELOPMENT_TEAM") } - func sortGroups(group: PBXGroup) { - // sort children - let children = group.children - .sorted { child1, child2 in - let sortOrder1 = child1.getSortOrder(groupSortPosition: project.options.groupSortPosition) - let sortOrder2 = child2.getSortOrder(groupSortPosition: project.options.groupSortPosition) - - if sortOrder1 != sortOrder2 { - return sortOrder1 < sortOrder2 - } else { - if (child1.name, child1.path) != (child2.name, child2.path) { - return PBXFileElement.sortByNamePath(child1, child2) - } else { - return child1.context ?? "" < child2.context ?? "" - } - } - } - group.children = children.filter { $0 != group } - - // sort sub groups - let childGroups = group.children.compactMap { $0 as? PBXGroup } - childGroups.forEach(sortGroups) + for target in project.aggregateTargets { + guard let pbxTarget = targetAggregateObjects[target.name] else { + continue + } + generateTargetAttributes(target, pbxTarget: pbxTarget) } - public func setupGroupOrdering(group: PBXGroup) { - let groupOrdering = project.options.groupOrdering.first { groupOrdering in - let groupName = group.nameOrPath + for target in project.targets { + guard let pbxTarget = targetObjects[target.name] else { + continue + } + generateTargetAttributes(target, pbxTarget: pbxTarget) + } - if groupName == groupOrdering.pattern { - return true - } + return targetAttributes + } - if let regex = groupOrdering.regex { - return regex.isMatch(to: groupName) - } + func sortGroups(group: PBXGroup) { + // sort children + let children = group.children + .sorted { child1, child2 in + let sortOrder1 = child1.getSortOrder(groupSortPosition: project.options.groupSortPosition) + let sortOrder2 = child2.getSortOrder(groupSortPosition: project.options.groupSortPosition) - return false + if sortOrder1 != sortOrder2 { + return sortOrder1 < sortOrder2 + } else { + if (child1.name, child1.path) != (child2.name, child2.path) { + return PBXFileElement.sortByNamePath(child1, child2) + } else { + return child1.context ?? "" < child2.context ?? "" + } } + } + group.children = children.filter { $0 != group } - if let order = groupOrdering?.order { - let files = group.children.filter { !$0.isGroupOrFolder } - var groups = group.children.filter { $0.isGroupOrFolder } + // sort sub groups + let childGroups = group.children.compactMap { $0 as? PBXGroup } + childGroups.forEach(sortGroups) + } - var filteredGroups = [PBXFileElement]() + public func setupGroupOrdering(group: PBXGroup) { + let groupOrdering = project.options.groupOrdering.first { groupOrdering in + let groupName = group.nameOrPath - for groupName in order { - guard let group = groups.first(where: { $0.nameOrPath == groupName }) else { - continue - } - - filteredGroups.append(group) - groups.removeAll { $0 == group } - } + if groupName == groupOrdering.pattern { + return true + } - filteredGroups += groups + if let regex = groupOrdering.regex { + return regex.isMatch(to: groupName) + } - switch project.options.groupSortPosition { - case .top: - group.children = filteredGroups + files - case .bottom: - group.children = files + filteredGroups - default: - break - } - } - - // sort sub groups - let childGroups = group.children.compactMap { $0 as? PBXGroup } - childGroups.forEach(setupGroupOrdering) + return false } - func getPBXProj(from reference: ProjectReference) throws -> PBXProj { - if let cachedProject = projects[reference] { - return cachedProject - } - let pbxproj = try XcodeProj(pathString: (project.basePath + Path(reference.path).normalize()).string).pbxproj - projects[reference] = pbxproj - return pbxproj - } - - func generateTarget(_ target: Target) throws { - let carthageDependencies = carthageResolver.dependencies(for: target) - - let infoPlistFiles: [Config: String] = getInfoPlists(for: target) - let sourceFileBuildPhaseOverrideSequence: [(Path, BuildPhaseSpec)] = Set(infoPlistFiles.values).map({ (project.basePath + $0, .none) }) - let sourceFileBuildPhaseOverrides = Dictionary(uniqueKeysWithValues: sourceFileBuildPhaseOverrideSequence) - let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources, platform: target.platform, buildPhases: sourceFileBuildPhaseOverrides) - .sorted { $0.path.lastComponent < $1.path.lastComponent } - - var anyDependencyRequiresObjCLinking = false - - var dependencies: [PBXTargetDependency] = [] - var targetFrameworkBuildFiles: [PBXBuildFile] = [] - var frameworkBuildPaths = Set() - var customCopyDependenciesReferences: [PBXBuildFile] = [] - var copyFilesBuildPhasesFiles: [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] = [:] - var copyFrameworksReferences: [PBXBuildFile] = [] - var copyResourcesReferences: [PBXBuildFile] = [] - var copyBundlesReferences: [PBXBuildFile] = [] - var copyWatchReferences: [PBXBuildFile] = [] - var packageDependencies: [XCSwiftPackageProductDependency] = [] - var extensions: [PBXBuildFile] = [] - var extensionKitExtensions: [PBXBuildFile] = [] - var systemExtensions: [PBXBuildFile] = [] - var appClips: [PBXBuildFile] = [] - var carthageFrameworksToEmbed: [String] = [] - - let targetDependencies = (target.transitivelyLinkDependencies ?? project.options.transitivelyLinkDependencies) ? - getAllDependenciesPlusTransitiveNeedingEmbedding(target: target) : target.dependencies - - let targetSupportsDirectEmbed = !(target.platform.requiresSimulatorStripping && - (target.type.isApp || target.type == .watch2Extension)) - let directlyEmbedCarthage = target.directlyEmbedCarthageDependencies ?? targetSupportsDirectEmbed - - func getEmbedSettings(dependency: Dependency, codeSign: Bool) -> [String: Any] { - var embedAttributes: [String] = [] - if codeSign { - embedAttributes.append("CodeSignOnCopy") - } - if dependency.removeHeaders { - embedAttributes.append("RemoveHeadersOnCopy") - } - var retval: [String:Any] = ["ATTRIBUTES": embedAttributes] - if let copyPhase = dependency.copyPhase { - retval["COPY_PHASE"] = copyPhase - } - return retval - } - - func getDependencyFrameworkSettings(dependency: Dependency) -> [String: Any]? { - var linkingAttributes: [String] = [] - if dependency.weakLink { - linkingAttributes.append("Weak") - } - return !linkingAttributes.isEmpty ? ["ATTRIBUTES": linkingAttributes] : nil - } + if let order = groupOrdering?.order { + let files = group.children.filter { !$0.isGroupOrFolder } + var groups = group.children.filter { $0.isGroupOrFolder } - func processTargetDependency(_ dependency: Dependency, dependencyTarget: Target, embedFileReference: PBXFileElement?, platform: String?, platforms: [String]?) { - let dependencyLinkage = dependencyTarget.defaultLinkage - let link = dependency.link ?? - ((dependencyLinkage == .dynamic && target.type != .staticLibrary) || - (dependencyLinkage == .static && target.type.isExecutable)) - - if link, let dependencyFile = embedFileReference { - let pbxBuildFile = PBXBuildFile(file: dependencyFile, settings: getDependencyFrameworkSettings(dependency: dependency)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let buildFile = addObject(pbxBuildFile) - targetFrameworkBuildFiles.append(buildFile) - - if !anyDependencyRequiresObjCLinking - && dependencyTarget.requiresObjCLinking ?? (dependencyTarget.type == .staticLibrary) { - anyDependencyRequiresObjCLinking = true - } - } + var filteredGroups = [PBXFileElement]() - let embed = dependency.embed ?? target.type.shouldEmbed(dependencyTarget) - if embed { - let pbxBuildFile = PBXBuildFile( - file: embedFileReference, - settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? !dependencyTarget.type.isExecutable) - ) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let embedFile = addObject(pbxBuildFile) - - if dependency.copyPhase != nil { - // custom copy takes precedence - customCopyDependenciesReferences.append(embedFile) - } else if dependencyTarget.type.isExtension { - if dependencyTarget.type == .extensionKitExtension { - // embed extension kit extension - extensionKitExtensions.append(embedFile) - } else { - // embed app extension - extensions.append(embedFile) - } - } else if dependencyTarget.type.isSystemExtension { - // embed system extension - systemExtensions.append(embedFile) - } else if dependencyTarget.type == .onDemandInstallCapableApplication { - // embed app clip - appClips.append(embedFile) - } else if dependencyTarget.type.isFramework { - copyFrameworksReferences.append(embedFile) - } else if dependencyTarget.type.isApp && dependencyTarget.platform == .watchOS { - copyWatchReferences.append(embedFile) - } else if dependencyTarget.type == .xpcService { - copyFilesBuildPhasesFiles[.xpcServices, default: []].append(embedFile) - } else { - copyResourcesReferences.append(embedFile) - } - } + for groupName in order { + guard let group = groups.first(where: { $0.nameOrPath == groupName }) else { + continue } - for dependency in targetDependencies { - - let embed = dependency.embed ?? target.shouldEmbedDependencies - let platform = makePlatformFilter(for: dependency.platformFilter) - let platforms = makeDestinationFilters(for: dependency.destinationFilters) - - switch dependency.type { - case .target: - let dependencyTargetReference = try TargetReference(dependency.reference) - - switch dependencyTargetReference.location { - case .local: - let dependencyTargetName = dependency.reference - let targetDependency = generateTargetDependency(from: target.name, to: dependencyTargetName, platform: platform, platforms: platforms) - dependencies.append(targetDependency) - guard let dependencyTarget = project.getTarget(dependencyTargetName) else { continue } - processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: targetFileReferences[dependencyTarget.name], platform: platform, platforms: platforms) - case .project(let dependencyProjectName): - let dependencyTargetName = dependencyTargetReference.name - let (targetDependency, dependencyTarget, dependencyProductProxy) = try generateExternalTargetDependency(from: target.name, to: dependencyTargetName, in: dependencyProjectName, platform: target.platform) - dependencies.append(targetDependency) - processTargetDependency(dependency, dependencyTarget: dependencyTarget, embedFileReference: dependencyProductProxy, platform: platform, platforms: platforms) - } - - case .framework: - if !dependency.implicit { - let buildPath = Path(dependency.reference).parent().string.quoted - frameworkBuildPaths.insert(buildPath) - } - - let fileReference: PBXFileElement - if dependency.implicit { - fileReference = sourceGenerator.getFileReference( - path: Path(dependency.reference), - inPath: project.basePath, - sourceTree: .buildProductsDir - ) - } else { - fileReference = sourceGenerator.getFileReference( - path: Path(dependency.reference), - inPath: project.basePath - ) - } + filteredGroups.append(group) + groups.removeAll { $0 == group } + } - if dependency.link ?? (target.type != .staticLibrary) { - let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let buildFile = addObject(pbxBuildFile) + filteredGroups += groups - targetFrameworkBuildFiles.append(buildFile) - } - - if !frameworkFiles.contains(fileReference) { - frameworkFiles.append(fileReference) - } - - if embed { - let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let embedFile = addObject(pbxBuildFile) - - if dependency.copyPhase != nil { - customCopyDependenciesReferences.append(embedFile) - } else { - copyFrameworksReferences.append(embedFile) - } - } - case .sdk(let root): - - var dependencyPath = Path(dependency.reference) - if !dependency.reference.contains("/") { - switch dependencyPath.extension ?? "" { - case "framework": - dependencyPath = Path("System/Library/Frameworks") + dependencyPath - case "tbd": - dependencyPath = Path("usr/lib") + dependencyPath - case "dylib": - dependencyPath = Path("usr/lib") + dependencyPath - default: break - } - } - - let fileReference: PBXFileReference - if let existingFileReferences = sdkFileReferences[dependency.reference] { - fileReference = existingFileReferences - } else { - let sourceTree: PBXSourceTree - if let root = root { - sourceTree = .custom(root) - } else { - sourceTree = .sdkRoot - } - fileReference = addObject( - PBXFileReference( - sourceTree: sourceTree, - name: dependencyPath.lastComponent, - lastKnownFileType: Xcode.fileType(path: dependencyPath), - path: dependencyPath.string - ) - ) - sdkFileReferences[dependency.reference] = fileReference - frameworkFiles.append(fileReference) - } - - let pbxBuildFile = PBXBuildFile( - file: fileReference, - settings: getDependencyFrameworkSettings(dependency: dependency) - ) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let buildFile = addObject(pbxBuildFile) - targetFrameworkBuildFiles.append(buildFile) - - if dependency.embed == true { - let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let embedFile = addObject(pbxBuildFile) - - if dependency.copyPhase != nil { - customCopyDependenciesReferences.append(embedFile) - } else { - copyFrameworksReferences.append(embedFile) - } - } - - case .carthage(let findFrameworks, let linkType): - let findFrameworks = findFrameworks ?? project.options.findCarthageFrameworks - let allDependencies = findFrameworks - ? carthageResolver.relatedDependencies(for: dependency, in: target.platform) : [dependency] - allDependencies.forEach { dependency in - - let platformPath = Path(carthageResolver.buildPath(for: target.platform, linkType: linkType)) - var frameworkPath = platformPath + dependency.reference - if frameworkPath.extension == nil { - frameworkPath = Path(frameworkPath.string + ".framework") - } - let fileReference = self.sourceGenerator.getFileReference(path: frameworkPath, inPath: platformPath) - - self.carthageFrameworksByPlatform[target.platform.carthageName, default: []].insert(fileReference) - - let isStaticLibrary = target.type == .staticLibrary - let isCarthageStaticLink = dependency.carthageLinkType == .static - if dependency.link ?? (!isStaticLibrary && !isCarthageStaticLink) { - let pbxBuildFile = PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let buildFile = addObject(pbxBuildFile) - targetFrameworkBuildFiles.append(buildFile) - } - } - // Embedding handled by iterating over `carthageDependencies` below - case .package(let products): - let packageReference = packageReferences[dependency.reference] - - // If package's reference is none and there is no specified package in localPackages, - // then ignore the package specified as dependency. - if packageReference == nil, localPackageReferences[dependency.reference] == nil { - continue - } - - func addPackageProductDependency(named productName: String) { - let packageDependency = addObject( - XCSwiftPackageProductDependency(productName: productName, package: packageReference) - ) - - // Add package dependency if linking is true. - if dependency.link ?? true { - packageDependencies.append(packageDependency) - } - - let link = dependency.link ?? (target.type != .staticLibrary) - if link { - let file = PBXBuildFile(product: packageDependency, settings: getDependencyFrameworkSettings(dependency: dependency)) - file.platformFilter = platform - file.platformFilters = platforms - let buildFile = addObject(file) - targetFrameworkBuildFiles.append(buildFile) - } else { - let targetDependency = addObject( - PBXTargetDependency(platformFilter: platform, platformFilters: platforms, product: packageDependency) - ) - dependencies.append(targetDependency) - } - - if dependency.embed == true { - let pbxBuildFile = PBXBuildFile(product: packageDependency, - settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let embedFile = addObject(pbxBuildFile) - - if dependency.copyPhase != nil { - customCopyDependenciesReferences.append(embedFile) - } else { - copyFrameworksReferences.append(embedFile) - } - } - } + switch project.options.groupSortPosition { + case .top: + group.children = filteredGroups + files + case .bottom: + group.children = files + filteredGroups + default: + break + } + } - if !products.isEmpty { - for product in products { - addPackageProductDependency(named: product) - } - } else { - addPackageProductDependency(named: dependency.reference) - } - case .bundle: - // Static and dynamic libraries can't copy resources - guard target.type != .staticLibrary && target.type != .dynamicLibrary else { break } - - let fileReference = sourceGenerator.getFileReference( - path: Path(dependency.reference), - inPath: project.basePath, - sourceTree: .buildProductsDir - ) - - let pbxBuildFile = PBXBuildFile( - file: fileReference, - settings: embed ? getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true) : nil - ) - pbxBuildFile.platformFilter = platform - pbxBuildFile.platformFilters = platforms - let buildFile = addObject(pbxBuildFile) - copyBundlesReferences.append(buildFile) - - if !bundleFiles.contains(fileReference) { - bundleFiles.append(fileReference) - } - } - } + // sort sub groups + let childGroups = group.children.compactMap { $0 as? PBXGroup } + childGroups.forEach(setupGroupOrdering) + } - for carthageDependency in carthageDependencies { - let dependency = carthageDependency.dependency - let isFromTopLevelTarget = carthageDependency.isFromTopLevelTarget - let embed = dependency.embed ?? target.shouldEmbedCarthageDependencies + func getPBXProj(from reference: ProjectReference) throws -> PBXProj { + if let cachedProject = projects[reference] { + return cachedProject + } + let pbxproj = try XcodeProj( + pathString: (project.basePath + Path(reference.path).normalize()).string + ).pbxproj + projects[reference] = pbxproj + return pbxproj + } + + func generateTarget(_ target: Target) throws { + let carthageDependencies = carthageResolver.dependencies(for: target) + + let infoPlistFiles: [Config: String] = getInfoPlists(for: target) + let sourceFileBuildPhaseOverrideSequence: [(Path, BuildPhaseSpec)] = Set(infoPlistFiles.values) + .map({ (project.basePath + $0, .none) }) + let sourceFileBuildPhaseOverrides = Dictionary( + uniqueKeysWithValues: sourceFileBuildPhaseOverrideSequence) + let sourceFiles = try sourceGenerator.getAllSourceFiles( + targetName: target.name, + targetType: target.type, + sources: target.sources, + platform: target.platform, + buildPhases: sourceFileBuildPhaseOverrides + ) + .sorted { $0.path.lastComponent < $1.path.lastComponent } + + var anyDependencyRequiresObjCLinking = false + + var dependencies: [PBXTargetDependency] = [] + var targetFrameworkBuildFiles: [PBXBuildFile] = [] + var frameworkBuildPaths = Set() + var customCopyDependenciesReferences: [PBXBuildFile] = [] + var copyFilesBuildPhasesFiles: [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] = [:] + var copyFrameworksReferences: [PBXBuildFile] = [] + var copyResourcesReferences: [PBXBuildFile] = [] + var copyBundlesReferences: [PBXBuildFile] = [] + var copyWatchReferences: [PBXBuildFile] = [] + var packageDependencies: [XCSwiftPackageProductDependency] = [] + var extensions: [PBXBuildFile] = [] + var extensionKitExtensions: [PBXBuildFile] = [] + var systemExtensions: [PBXBuildFile] = [] + var appClips: [PBXBuildFile] = [] + var carthageFrameworksToEmbed: [String] = [] + + let targetDependencies = + (target.transitivelyLinkDependencies ?? project.options.transitivelyLinkDependencies) + ? getAllDependenciesPlusTransitiveNeedingEmbedding(target: target) : target.dependencies + + let targetSupportsDirectEmbed = + !(target.platform.requiresSimulatorStripping + && (target.type.isApp || target.type == .watch2Extension)) + let directlyEmbedCarthage = + target.directlyEmbedCarthageDependencies ?? targetSupportsDirectEmbed + + func getEmbedSettings(dependency: Dependency, codeSign: Bool) -> [String: Any] { + var embedAttributes: [String] = [] + if codeSign { + embedAttributes.append("CodeSignOnCopy") + } + if dependency.removeHeaders { + embedAttributes.append("RemoveHeadersOnCopy") + } + var retval: [String: Any] = ["ATTRIBUTES": embedAttributes] + if let copyPhase = dependency.copyPhase { + retval["COPY_PHASE"] = copyPhase + } + return retval + } - let platformPath = Path(carthageResolver.buildPath(for: target.platform, linkType: dependency.carthageLinkType ?? .default)) - var frameworkPath = platformPath + dependency.reference - if frameworkPath.extension == nil { - frameworkPath = Path(frameworkPath.string + ".framework") - } - let fileReference = sourceGenerator.getFileReference(path: frameworkPath, inPath: platformPath) - - if dependency.carthageLinkType == .static { - guard isFromTopLevelTarget else { continue } // ignore transitive dependencies if static - let linkFile = addObject( - PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) - ) - targetFrameworkBuildFiles.append(linkFile) - } else if embed { - if directlyEmbedCarthage { - let embedFile = addObject( - PBXBuildFile(file: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true)) - ) - if dependency.copyPhase != nil { - customCopyDependenciesReferences.append(embedFile) - } else { - copyFrameworksReferences.append(embedFile) - } - } else { - carthageFrameworksToEmbed.append(dependency.reference) - } - } - } - - carthageFrameworksToEmbed = carthageFrameworksToEmbed.uniqued() - - let packagePluginDependencies = makePackagePluginDependency(for: target) - dependencies.append(contentsOf: packagePluginDependencies) - - var buildPhases: [PBXBuildPhase] = [] - - func getBuildFilesForSourceFiles(_ sourceFiles: [SourceFile]) -> [PBXBuildFile] { - sourceFiles - .reduce(into: [SourceFile]()) { output, sourceFile in - if !output.contains(where: { $0.fileReference === sourceFile.fileReference }) { - output.append(sourceFile) - } - } - .map { addObject($0.buildFile) } - } + func getDependencyFrameworkSettings(dependency: Dependency) -> [String: Any]? { + var linkingAttributes: [String] = [] + if dependency.weakLink { + linkingAttributes.append("Weak") + } + return !linkingAttributes.isEmpty ? ["ATTRIBUTES": linkingAttributes] : nil + } - func getBuildFilesForPhase(_ buildPhase: BuildPhase) -> [PBXBuildFile] { - let filteredSourceFiles = sourceFiles - .filter { $0.buildPhase?.buildPhase == buildPhase } - return getBuildFilesForSourceFiles(filteredSourceFiles) + func processTargetDependency( + _ dependency: Dependency, dependencyTarget: Target, embedFileReference: PBXFileElement?, + platform: String?, platforms: [String]? + ) { + let dependencyLinkage = dependencyTarget.defaultLinkage + let link = + dependency.link + ?? ((dependencyLinkage == .dynamic && target.type != .staticLibrary) + || (dependencyLinkage == .static && target.type.isExecutable)) + + if link, let dependencyFile = embedFileReference { + let pbxBuildFile = PBXBuildFile( + file: dependencyFile, settings: getDependencyFrameworkSettings(dependency: dependency)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let buildFile = addObject(pbxBuildFile) + targetFrameworkBuildFiles.append(buildFile) + + if !anyDependencyRequiresObjCLinking + && dependencyTarget.requiresObjCLinking ?? (dependencyTarget.type == .staticLibrary) + { + anyDependencyRequiresObjCLinking = true + } + } + + let embed = dependency.embed ?? target.type.shouldEmbed(dependencyTarget) + if embed { + let pbxBuildFile = PBXBuildFile( + file: embedFileReference, + settings: getEmbedSettings( + dependency: dependency, + codeSign: dependency.codeSign ?? !dependencyTarget.type.isExecutable) + ) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let embedFile = addObject(pbxBuildFile) + + if dependency.copyPhase != nil { + // custom copy takes precedence + customCopyDependenciesReferences.append(embedFile) + } else if dependencyTarget.type.isExtension { + if dependencyTarget.type == .extensionKitExtension { + // embed extension kit extension + extensionKitExtensions.append(embedFile) + } else { + // embed app extension + extensions.append(embedFile) + } + } else if dependencyTarget.type.isSystemExtension { + // embed system extension + systemExtensions.append(embedFile) + } else if dependencyTarget.type == .onDemandInstallCapableApplication { + // embed app clip + appClips.append(embedFile) + } else if dependencyTarget.type.isFramework { + copyFrameworksReferences.append(embedFile) + } else if dependencyTarget.type.isApp && dependencyTarget.platform == .watchOS { + copyWatchReferences.append(embedFile) + } else if dependencyTarget.type == .xpcService { + copyFilesBuildPhasesFiles[.xpcServices, default: []].append(embedFile) + } else { + copyResourcesReferences.append(embedFile) } + } + } - func getBuildFilesForCopyFilesPhases() -> [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] { - var sourceFilesByCopyFiles: [BuildPhaseSpec.CopyFilesSettings: [SourceFile]] = [:] - for sourceFile in sourceFiles { - guard case let .copyFiles(copyFilesSettings)? = sourceFile.buildPhase else { continue } - sourceFilesByCopyFiles[copyFilesSettings, default: []].append(sourceFile) - } - return sourceFilesByCopyFiles.mapValues { getBuildFilesForSourceFiles($0) } + for dependency in targetDependencies { + + let embed = dependency.embed ?? target.shouldEmbedDependencies + let platform = makePlatformFilter(for: dependency.platformFilter) + let platforms = makeDestinationFilters(for: dependency.destinationFilters) + + switch dependency.type { + case .target: + let dependencyTargetReference = try TargetReference(dependency.reference) + + switch dependencyTargetReference.location { + case .local: + let dependencyTargetName = dependency.reference + let targetDependency = generateTargetDependency( + from: target.name, to: dependencyTargetName, platform: platform, platforms: platforms) + dependencies.append(targetDependency) + guard let dependencyTarget = project.getTarget(dependencyTargetName) else { continue } + processTargetDependency( + dependency, dependencyTarget: dependencyTarget, + embedFileReference: targetFileReferences[dependencyTarget.name], platform: platform, + platforms: platforms) + case .project(let dependencyProjectName): + let dependencyTargetName = dependencyTargetReference.name + let (targetDependency, dependencyTarget, dependencyProductProxy) = + try generateExternalTargetDependency( + from: target.name, to: dependencyTargetName, in: dependencyProjectName, + platform: target.platform) + dependencies.append(targetDependency) + processTargetDependency( + dependency, dependencyTarget: dependencyTarget, + embedFileReference: dependencyProductProxy, platform: platform, platforms: platforms) + } + + case .framework: + if !dependency.implicit { + let buildPath = Path(dependency.reference).parent().string.quoted + frameworkBuildPaths.insert(buildPath) + } + + let fileReference: PBXFileElement + if dependency.implicit { + fileReference = sourceGenerator.getFileReference( + path: Path(dependency.reference), + inPath: project.basePath, + sourceTree: .buildProductsDir + ) + } else { + fileReference = sourceGenerator.getFileReference( + path: Path(dependency.reference), + inPath: project.basePath + ) + } + + if dependency.link ?? (target.type != .staticLibrary) { + let pbxBuildFile = PBXBuildFile( + file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let buildFile = addObject(pbxBuildFile) + + targetFrameworkBuildFiles.append(buildFile) + } + + if !frameworkFiles.contains(fileReference) { + frameworkFiles.append(fileReference) + } + + if embed { + let pbxBuildFile = PBXBuildFile( + file: fileReference, + settings: getEmbedSettings( + dependency: dependency, codeSign: dependency.codeSign ?? true)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let embedFile = addObject(pbxBuildFile) + + if dependency.copyPhase != nil { + customCopyDependenciesReferences.append(embedFile) + } else { + copyFrameworksReferences.append(embedFile) + } + } + case .sdk(let root): + + var dependencyPath = Path(dependency.reference) + if !dependency.reference.contains("/") { + switch dependencyPath.extension ?? "" { + case "framework": + dependencyPath = Path("System/Library/Frameworks") + dependencyPath + case "tbd": + dependencyPath = Path("usr/lib") + dependencyPath + case "dylib": + dependencyPath = Path("usr/lib") + dependencyPath + default: break + } + } + + let fileReference: PBXFileReference + if let existingFileReferences = sdkFileReferences[dependency.reference] { + fileReference = existingFileReferences + } else { + let sourceTree: PBXSourceTree + if let root = root { + sourceTree = .custom(root) + } else { + sourceTree = .sdkRoot + } + fileReference = addObject( + PBXFileReference( + sourceTree: sourceTree, + name: dependencyPath.lastComponent, + lastKnownFileType: Xcode.fileType(path: dependencyPath), + path: dependencyPath.string + ) + ) + sdkFileReferences[dependency.reference] = fileReference + frameworkFiles.append(fileReference) } - func getPBXCopyFilesBuildPhase(dstSubfolderSpec: PBXCopyFilesBuildPhase.SubFolder, dstPath: String = "", name: String, files: [PBXBuildFile]) -> PBXCopyFilesBuildPhase { - return PBXCopyFilesBuildPhase( - dstPath: dstPath, - dstSubfolderSpec: dstSubfolderSpec, - name: name, - buildActionMask: target.onlyCopyFilesOnInstall ? PBXProjGenerator.copyFilesActionMask : PBXBuildPhase.defaultBuildActionMask, - files: files, - runOnlyForDeploymentPostprocessing: target.onlyCopyFilesOnInstall ? true : false + let pbxBuildFile = PBXBuildFile( + file: fileReference, + settings: getDependencyFrameworkSettings(dependency: dependency) + ) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let buildFile = addObject(pbxBuildFile) + targetFrameworkBuildFiles.append(buildFile) + + if dependency.embed == true { + let pbxBuildFile = PBXBuildFile( + file: fileReference, + settings: getEmbedSettings( + dependency: dependency, codeSign: dependency.codeSign ?? true)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let embedFile = addObject(pbxBuildFile) + + if dependency.copyPhase != nil { + customCopyDependenciesReferences.append(embedFile) + } else { + copyFrameworksReferences.append(embedFile) + } + } + + case .carthage(let findFrameworks, let linkType): + let findFrameworks = findFrameworks ?? project.options.findCarthageFrameworks + let allDependencies = + findFrameworks + ? carthageResolver.relatedDependencies(for: dependency, in: target.platform) + : [dependency] + allDependencies.forEach { dependency in + + let platformPath = Path( + carthageResolver.buildPath(for: target.platform, linkType: linkType)) + var frameworkPath = platformPath + dependency.reference + if frameworkPath.extension == nil { + frameworkPath = Path(frameworkPath.string + ".framework") + } + let fileReference = self.sourceGenerator.getFileReference( + path: frameworkPath, inPath: platformPath) + + self.carthageFrameworksByPlatform[target.platform.carthageName, default: []].insert( + fileReference) + + let isStaticLibrary = target.type == .staticLibrary + let isCarthageStaticLink = dependency.carthageLinkType == .static + if dependency.link ?? (!isStaticLibrary && !isCarthageStaticLink) { + let pbxBuildFile = PBXBuildFile( + file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let buildFile = addObject(pbxBuildFile) + targetFrameworkBuildFiles.append(buildFile) + } + } + // Embedding handled by iterating over `carthageDependencies` below + case .package(let products): + let packageReference = packageReferences[dependency.reference] + + // If package's reference is none and there is no specified package in localPackages, + // then ignore the package specified as dependency. + if packageReference == nil, localPackageReferences[dependency.reference] == nil { + continue + } + + func addPackageProductDependency(named productName: String) { + let packageDependency = addObject( + XCSwiftPackageProductDependency(productName: productName, package: packageReference) + ) + + // Add package dependency if linking is true. + if dependency.link ?? true { + packageDependencies.append(packageDependency) + } + + let link = dependency.link ?? (target.type != .staticLibrary) + if link { + let file = PBXBuildFile( + product: packageDependency, + settings: getDependencyFrameworkSettings(dependency: dependency)) + file.platformFilter = platform + file.platformFilters = platforms + let buildFile = addObject(file) + targetFrameworkBuildFiles.append(buildFile) + } else { + let targetDependency = addObject( + PBXTargetDependency( + platformFilter: platform, platformFilters: platforms, product: packageDependency) ) - } - - func splitCopyDepsByDestination(_ references: [PBXBuildFile]) -> [BuildPhaseSpec.CopyFilesSettings : [PBXBuildFile]] { - - var retval = [BuildPhaseSpec.CopyFilesSettings : [PBXBuildFile]]() - for reference in references { - - guard let key = reference.settings?["COPY_PHASE"] as? BuildPhaseSpec.CopyFilesSettings else { continue } - var filesWithSameDestination = retval[key] ?? [PBXBuildFile]() - filesWithSameDestination.append(reference) - retval[key] = filesWithSameDestination + dependencies.append(targetDependency) + } + + if dependency.embed == true { + let pbxBuildFile = PBXBuildFile( + product: packageDependency, + settings: getEmbedSettings( + dependency: dependency, codeSign: dependency.codeSign ?? true)) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let embedFile = addObject(pbxBuildFile) + + if dependency.copyPhase != nil { + customCopyDependenciesReferences.append(embedFile) + } else { + copyFrameworksReferences.append(embedFile) } - return retval + } } - - copyFilesBuildPhasesFiles.merge(getBuildFilesForCopyFilesPhases()) { $0 + $1 } - buildPhases += try target.preBuildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) } + if !products.isEmpty { + for product in products { + addPackageProductDependency(named: product) + } + } else { + addPackageProductDependency(named: dependency.reference) + } + case .bundle: + // Static and dynamic libraries can't copy resources + guard target.type != .staticLibrary && target.type != .dynamicLibrary else { break } - buildPhases += copyFilesBuildPhasesFiles - .filter { $0.key.phaseOrder == .preCompile } - .map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) } + let fileReference = sourceGenerator.getFileReference( + path: Path(dependency.reference), + inPath: project.basePath, + sourceTree: .buildProductsDir + ) - let headersBuildPhaseFiles = getBuildFilesForPhase(.headers) - if !headersBuildPhaseFiles.isEmpty { - if target.type.isFramework || target.type == .dynamicLibrary { - let headersBuildPhase = addObject(PBXHeadersBuildPhase(files: headersBuildPhaseFiles)) - buildPhases.append(headersBuildPhase) - } else { - headersBuildPhaseFiles.forEach { pbxProj.delete(object: $0) } - } - } + let pbxBuildFile = PBXBuildFile( + file: fileReference, + settings: embed + ? getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true) : nil + ) + pbxBuildFile.platformFilter = platform + pbxBuildFile.platformFilters = platforms + let buildFile = addObject(pbxBuildFile) + copyBundlesReferences.append(buildFile) - func addResourcesBuildPhase() { - let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences - if !resourcesBuildPhaseFiles.isEmpty { - let resourcesBuildPhase = addObject(PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles)) - buildPhases.append(resourcesBuildPhase) - } + if !bundleFiles.contains(fileReference) { + bundleFiles.append(fileReference) } + } + } - if target.putResourcesBeforeSourcesBuildPhase { - addResourcesBuildPhase() + for carthageDependency in carthageDependencies { + let dependency = carthageDependency.dependency + let isFromTopLevelTarget = carthageDependency.isFromTopLevelTarget + let embed = dependency.embed ?? target.shouldEmbedCarthageDependencies + + let platformPath = Path( + carthageResolver.buildPath( + for: target.platform, linkType: dependency.carthageLinkType ?? .default)) + var frameworkPath = platformPath + dependency.reference + if frameworkPath.extension == nil { + frameworkPath = Path(frameworkPath.string + ".framework") + } + let fileReference = sourceGenerator.getFileReference( + path: frameworkPath, inPath: platformPath) + + if dependency.carthageLinkType == .static { + guard isFromTopLevelTarget else { continue } // ignore transitive dependencies if static + let linkFile = addObject( + PBXBuildFile( + file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency)) + ) + targetFrameworkBuildFiles.append(linkFile) + } else if embed { + if directlyEmbedCarthage { + let embedFile = addObject( + PBXBuildFile( + file: fileReference, + settings: getEmbedSettings( + dependency: dependency, codeSign: dependency.codeSign ?? true)) + ) + if dependency.copyPhase != nil { + customCopyDependenciesReferences.append(embedFile) + } else { + copyFrameworksReferences.append(embedFile) + } + } else { + carthageFrameworksToEmbed.append(dependency.reference) } + } + } - let sourcesBuildPhaseFiles = getBuildFilesForPhase(.sources) - let shouldSkipSourcesBuildPhase = sourcesBuildPhaseFiles.isEmpty && target.type.canSkipCompileSourcesBuildPhase - if !shouldSkipSourcesBuildPhase { - let sourcesBuildPhase = addObject(PBXSourcesBuildPhase(files: sourcesBuildPhaseFiles)) - buildPhases.append(sourcesBuildPhase) - } + carthageFrameworksToEmbed = carthageFrameworksToEmbed.uniqued() - buildPhases += try target.postCompileScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) } + let packagePluginDependencies = makePackagePluginDependency(for: target) + dependencies.append(contentsOf: packagePluginDependencies) - if !target.putResourcesBeforeSourcesBuildPhase { - addResourcesBuildPhase() - } + var buildPhases: [PBXBuildPhase] = [] - let swiftObjCInterfaceHeader = project.getCombinedBuildSetting("SWIFT_OBJC_INTERFACE_HEADER_NAME", target: target, config: project.configs[0]) as? String - let swiftInstallObjCHeader = project.getBoolBuildSetting("SWIFT_INSTALL_OBJC_HEADER", target: target, config: project.configs[0]) ?? true // Xcode default - - if target.type == .staticLibrary - && swiftObjCInterfaceHeader != "" - && swiftInstallObjCHeader - && sourceFiles.contains(where: { $0.buildPhase == .sources && $0.path.extension == "swift" }) { - - let inputPaths = ["$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"] - let outputPaths = ["$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"] - let script = addObject( - PBXShellScriptBuildPhase( - name: "Copy Swift Objective-C Interface Header", - inputPaths: inputPaths, - outputPaths: outputPaths, - shellPath: "/bin/sh", - shellScript: "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n" - ) - ) - buildPhases.append(script) + func getBuildFilesForSourceFiles(_ sourceFiles: [SourceFile]) -> [PBXBuildFile] { + sourceFiles + .reduce(into: [SourceFile]()) { output, sourceFile in + if !output.contains(where: { $0.fileReference === sourceFile.fileReference }) { + output.append(sourceFile) + } } + .map { addObject($0.buildFile) } + } - buildPhases += copyFilesBuildPhasesFiles - .filter { $0.key.phaseOrder == .postCompile } - .map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) } - - if !carthageFrameworksToEmbed.isEmpty { - - let inputPaths = carthageFrameworksToEmbed - .map { "$(SRCROOT)/\(carthageResolver.buildPath(for: target.platform, linkType: .dynamic))/\($0)\($0.contains(".") ? "" : ".framework")" } - let outputPaths = carthageFrameworksToEmbed - .map { "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/\($0)\($0.contains(".") ? "" : ".framework")" } - let carthageExecutable = carthageResolver.executable - let carthageScript = addObject( - PBXShellScriptBuildPhase( - name: "Carthage", - inputPaths: inputPaths, - outputPaths: outputPaths, - shellPath: "/bin/sh -l", - shellScript: "\(carthageExecutable) copy-frameworks\n" - ) - ) - buildPhases.append(carthageScript) - } + func getBuildFilesForPhase(_ buildPhase: BuildPhase) -> [PBXBuildFile] { + let filteredSourceFiles = + sourceFiles + .filter { $0.buildPhase?.buildPhase == buildPhase } + return getBuildFilesForSourceFiles(filteredSourceFiles) + } - if !targetFrameworkBuildFiles.isEmpty { + func getBuildFilesForCopyFilesPhases() -> [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] { + var sourceFilesByCopyFiles: [BuildPhaseSpec.CopyFilesSettings: [SourceFile]] = [:] + for sourceFile in sourceFiles { + guard case let .copyFiles(copyFilesSettings)? = sourceFile.buildPhase else { continue } + sourceFilesByCopyFiles[copyFilesSettings, default: []].append(sourceFile) + } + return sourceFilesByCopyFiles.mapValues { getBuildFilesForSourceFiles($0) } + } - let frameworkBuildPhase = addObject( - PBXFrameworksBuildPhase(files: targetFrameworkBuildFiles) - ) - buildPhases.append(frameworkBuildPhase) - } + func getPBXCopyFilesBuildPhase( + dstSubfolderSpec: PBXCopyFilesBuildPhase.SubFolder, dstPath: String = "", name: String, + files: [PBXBuildFile] + ) -> PBXCopyFilesBuildPhase { + return PBXCopyFilesBuildPhase( + dstPath: dstPath, + dstSubfolderSpec: dstSubfolderSpec, + name: name, + buildActionMask: target.onlyCopyFilesOnInstall + ? PBXProjGenerator.copyFilesActionMask : PBXBuildPhase.defaultBuildActionMask, + files: files, + runOnlyForDeploymentPostprocessing: target.onlyCopyFilesOnInstall ? true : false + ) + } - if !copyBundlesReferences.isEmpty { - let copyBundlesPhase = addObject(PBXCopyFilesBuildPhase( - dstSubfolderSpec: .resources, - name: "Copy Bundle Resources", - files: copyBundlesReferences - )) - buildPhases.append(copyBundlesPhase) - } + func splitCopyDepsByDestination(_ references: [PBXBuildFile]) -> [BuildPhaseSpec + .CopyFilesSettings: [PBXBuildFile]] + { - if !extensions.isEmpty { + var retval = [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]]() + for reference in references { - let copyFilesPhase = addObject( - getPBXCopyFilesBuildPhase(dstSubfolderSpec: .plugins, name: "Embed Foundation Extensions", files: extensions) - ) + guard let key = reference.settings?["COPY_PHASE"] as? BuildPhaseSpec.CopyFilesSettings + else { continue } + var filesWithSameDestination = retval[key] ?? [PBXBuildFile]() + filesWithSameDestination.append(reference) + retval[key] = filesWithSameDestination + } + return retval + } - buildPhases.append(copyFilesPhase) - } + copyFilesBuildPhasesFiles.merge(getBuildFilesForCopyFilesPhases()) { $0 + $1 } - if !extensionKitExtensions.isEmpty { + buildPhases += try target.preBuildScripts.map { + try generateBuildScript(targetName: target.name, buildScript: $0) + } - let copyFilesPhase = addObject( - getPBXCopyFilesBuildPhase(dstSubfolderSpec: .productsDirectory, dstPath: "$(EXTENSIONS_FOLDER_PATH)", name: "Embed ExtensionKit Extensions", files: extensionKitExtensions) - ) - buildPhases.append(copyFilesPhase) - } + buildPhases += + copyFilesBuildPhasesFiles + .filter { $0.key.phaseOrder == .preCompile } + .map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) } + + let headersBuildPhaseFiles = getBuildFilesForPhase(.headers) + if !headersBuildPhaseFiles.isEmpty { + if target.type.isFramework || target.type == .dynamicLibrary { + let headersBuildPhase = addObject(PBXHeadersBuildPhase(files: headersBuildPhaseFiles)) + buildPhases.append(headersBuildPhase) + } else { + headersBuildPhaseFiles.forEach { pbxProj.delete(object: $0) } + } + } - if !systemExtensions.isEmpty { + func addResourcesBuildPhase() { + let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences + if !resourcesBuildPhaseFiles.isEmpty { + let resourcesBuildPhase = addObject(PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles)) + buildPhases.append(resourcesBuildPhase) + } + } - let copyFilesPhase = addObject( - // With parameters below the Xcode will show "Destination: System Extensions". - getPBXCopyFilesBuildPhase(dstSubfolderSpec: .productsDirectory, dstPath: "$(SYSTEM_EXTENSIONS_FOLDER_PATH)", name: "Embed System Extensions", files: systemExtensions) - ) + if target.putResourcesBeforeSourcesBuildPhase { + addResourcesBuildPhase() + } - buildPhases.append(copyFilesPhase) - } + let sourcesBuildPhaseFiles = getBuildFilesForPhase(.sources) + let shouldSkipSourcesBuildPhase = + sourcesBuildPhaseFiles.isEmpty && target.type.canSkipCompileSourcesBuildPhase + if !shouldSkipSourcesBuildPhase { + let sourcesBuildPhase = addObject(PBXSourcesBuildPhase(files: sourcesBuildPhaseFiles)) + buildPhases.append(sourcesBuildPhase) + } - if !appClips.isEmpty { + buildPhases += try target.postCompileScripts.map { + try generateBuildScript(targetName: target.name, buildScript: $0) + } - let copyFilesPhase = addObject( - PBXCopyFilesBuildPhase( - dstPath: "$(CONTENTS_FOLDER_PATH)/AppClips", - dstSubfolderSpec: .productsDirectory, - name: "Embed App Clips", - files: appClips - ) - ) + if !target.putResourcesBeforeSourcesBuildPhase { + addResourcesBuildPhase() + } - buildPhases.append(copyFilesPhase) - } + let swiftObjCInterfaceHeader = + project.getCombinedBuildSetting( + "SWIFT_OBJC_INTERFACE_HEADER_NAME", target: target, config: project.configs[0]) as? String + let swiftInstallObjCHeader = + project.getBoolBuildSetting( + "SWIFT_INSTALL_OBJC_HEADER", target: target, config: project.configs[0]) ?? true // Xcode default + + if target.type == .staticLibrary + && swiftObjCInterfaceHeader != "" + && swiftInstallObjCHeader + && sourceFiles.contains(where: { $0.buildPhase == .sources && $0.path.extension == "swift" }) + { + + let inputPaths = ["$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"] + let outputPaths = [ + "$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)" + ] + let script = addObject( + PBXShellScriptBuildPhase( + name: "Copy Swift Objective-C Interface Header", + inputPaths: inputPaths, + outputPaths: outputPaths, + shellPath: "/bin/sh", + shellScript: "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n" + ) + ) + buildPhases.append(script) + } - copyFrameworksReferences += getBuildFilesForPhase(.frameworks) - if !copyFrameworksReferences.isEmpty { + buildPhases += + copyFilesBuildPhasesFiles + .filter { $0.key.phaseOrder == .postCompile } + .map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) } + + if !carthageFrameworksToEmbed.isEmpty { + + let inputPaths = + carthageFrameworksToEmbed + .map { + "$(SRCROOT)/\(carthageResolver.buildPath(for: target.platform, linkType: .dynamic))/\($0)\($0.contains(".") ? "" : ".framework")" + } + let outputPaths = + carthageFrameworksToEmbed + .map { + "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/\($0)\($0.contains(".") ? "" : ".framework")" + } + let carthageExecutable = carthageResolver.executable + let carthageScript = addObject( + PBXShellScriptBuildPhase( + name: "Carthage", + inputPaths: inputPaths, + outputPaths: outputPaths, + shellPath: "/bin/sh -l", + shellScript: "\(carthageExecutable) copy-frameworks\n" + ) + ) + buildPhases.append(carthageScript) + } - let copyFilesPhase = addObject( - getPBXCopyFilesBuildPhase(dstSubfolderSpec: .frameworks, name: "Embed Frameworks", files: copyFrameworksReferences) - ) + if !targetFrameworkBuildFiles.isEmpty { - buildPhases.append(copyFilesPhase) - } + let frameworkBuildPhase = addObject( + PBXFrameworksBuildPhase(files: targetFrameworkBuildFiles) + ) + buildPhases.append(frameworkBuildPhase) + } - if !customCopyDependenciesReferences.isEmpty { - - let splitted = splitCopyDepsByDestination(customCopyDependenciesReferences) - for (phase, references) in splitted { - - guard let destination = phase.destination.destination else { continue } - - let copyFilesPhase = addObject( - getPBXCopyFilesBuildPhase(dstSubfolderSpec: destination, dstPath:phase.subpath, name: "Embed Dependencies", files: references) - ) - - buildPhases.append(copyFilesPhase) - } - } - - if !copyWatchReferences.isEmpty { - - let copyFilesPhase = addObject( - PBXCopyFilesBuildPhase( - dstPath: "$(CONTENTS_FOLDER_PATH)/Watch", - dstSubfolderSpec: .productsDirectory, - name: "Embed Watch Content", - files: copyWatchReferences - ) - ) + if !copyBundlesReferences.isEmpty { + let copyBundlesPhase = addObject( + PBXCopyFilesBuildPhase( + dstSubfolderSpec: .resources, + name: "Copy Bundle Resources", + files: copyBundlesReferences + )) + buildPhases.append(copyBundlesPhase) + } - buildPhases.append(copyFilesPhase) - } + if !extensions.isEmpty { - let buildRules = target.buildRules.map { buildRule in - addObject( - PBXBuildRule( - compilerSpec: buildRule.action.compilerSpec, - fileType: buildRule.fileType.fileType, - isEditable: true, - filePatterns: buildRule.fileType.pattern, - name: buildRule.name ?? "Build Rule", - outputFiles: buildRule.outputFiles, - outputFilesCompilerFlags: buildRule.outputFilesCompilerFlags, - script: buildRule.action.script, - runOncePerArchitecture: buildRule.runOncePerArchitecture - ) - ) - } + let copyFilesPhase = addObject( + getPBXCopyFilesBuildPhase( + dstSubfolderSpec: .plugins, name: "Embed Foundation Extensions", files: extensions) + ) - buildPhases += try target.postBuildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) } + buildPhases.append(copyFilesPhase) + } - let configs: [XCBuildConfiguration] = project.configs.map { config in - var buildSettings = project.getTargetBuildSettings(target: target, config: config) + if !extensionKitExtensions.isEmpty { - // Set CODE_SIGN_ENTITLEMENTS - if let entitlements = target.entitlements { - buildSettings["CODE_SIGN_ENTITLEMENTS"] = entitlements.path - } + let copyFilesPhase = addObject( + getPBXCopyFilesBuildPhase( + dstSubfolderSpec: .productsDirectory, dstPath: "$(EXTENSIONS_FOLDER_PATH)", + name: "Embed ExtensionKit Extensions", files: extensionKitExtensions) + ) + buildPhases.append(copyFilesPhase) + } - // Set INFOPLIST_FILE based on the resolved value - if let infoPlistFile = infoPlistFiles[config] { - buildSettings["INFOPLIST_FILE"] = infoPlistFile - } + if !systemExtensions.isEmpty { - // automatically calculate bundle id - if let bundleIdPrefix = project.options.bundleIdPrefix, - !project.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", target: target, config: config) { - let characterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted - let escapedTargetName = target.name - .replacingOccurrences(of: "_", with: "-") - .components(separatedBy: characterSet) - .joined(separator: "") - buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = bundleIdPrefix + "." + escapedTargetName - } + let copyFilesPhase = addObject( + // With parameters below the Xcode will show "Destination: System Extensions". + getPBXCopyFilesBuildPhase( + dstSubfolderSpec: .productsDirectory, dstPath: "$(SYSTEM_EXTENSIONS_FOLDER_PATH)", + name: "Embed System Extensions", files: systemExtensions) + ) - // automatically set test target name - if target.type == .uiTestBundle, - !project.targetHasBuildSetting("TEST_TARGET_NAME", target: target, config: config) { - for dependency in target.dependencies { - if dependency.type == .target, - let dependencyTarget = project.getTarget(dependency.reference), - dependencyTarget.type.isApp { - buildSettings["TEST_TARGET_NAME"] = dependencyTarget.name - break - } - } - } + buildPhases.append(copyFilesPhase) + } - // automatically set TEST_HOST - if target.type == .unitTestBundle, - !project.targetHasBuildSetting("TEST_HOST", target: target, config: config) { - for dependency in target.dependencies { - if dependency.type == .target, - let dependencyTarget = project.getTarget(dependency.reference), - dependencyTarget.type.isApp { - if dependencyTarget.platform == .macOS { - buildSettings["TEST_HOST"] = "$(BUILT_PRODUCTS_DIR)/\(dependencyTarget.productName).app/Contents/MacOS/\(dependencyTarget.productName)" - } else { - buildSettings["TEST_HOST"] = "$(BUILT_PRODUCTS_DIR)/\(dependencyTarget.productName).app/\(dependencyTarget.productName)" - } - break - } - } - } + if !appClips.isEmpty { - // objc linkage - if anyDependencyRequiresObjCLinking { - let otherLinkingFlags = "OTHER_LDFLAGS" - let objCLinking = "-ObjC" - if var array = buildSettings[otherLinkingFlags] as? [String] { - array.append(objCLinking) - buildSettings[otherLinkingFlags] = array - } else if let string = buildSettings[otherLinkingFlags] as? String { - buildSettings[otherLinkingFlags] = [string, objCLinking] - } else { - buildSettings[otherLinkingFlags] = ["$(inherited)", objCLinking] - } - } + let copyFilesPhase = addObject( + PBXCopyFilesBuildPhase( + dstPath: "$(CONTENTS_FOLDER_PATH)/AppClips", + dstSubfolderSpec: .productsDirectory, + name: "Embed App Clips", + files: appClips + ) + ) - // set Carthage search paths - let configFrameworkBuildPaths: [String] - if !carthageDependencies.isEmpty { - var carthagePlatformBuildPaths: Set = [] - if carthageDependencies.contains(where: { $0.dependency.carthageLinkType == .static }) { - let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + carthageResolver.buildPath(for: target.platform, linkType: .static) - carthagePlatformBuildPaths.insert(carthagePlatformBuildPath) - } - if carthageDependencies.contains(where: { $0.dependency.carthageLinkType == .dynamic }) { - let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + carthageResolver.buildPath(for: target.platform, linkType: .dynamic) - carthagePlatformBuildPaths.insert(carthagePlatformBuildPath) - } - configFrameworkBuildPaths = carthagePlatformBuildPaths.sorted() + frameworkBuildPaths.sorted() - } else { - configFrameworkBuildPaths = frameworkBuildPaths.sorted() - } + buildPhases.append(copyFilesPhase) + } - // set framework search paths - if !configFrameworkBuildPaths.isEmpty { - let frameworkSearchPaths = "FRAMEWORK_SEARCH_PATHS" - if var array = buildSettings[frameworkSearchPaths] as? [String] { - array.append(contentsOf: configFrameworkBuildPaths) - buildSettings[frameworkSearchPaths] = array - } else if let string = buildSettings[frameworkSearchPaths] as? String { - buildSettings[frameworkSearchPaths] = [string] + configFrameworkBuildPaths - } else { - buildSettings[frameworkSearchPaths] = ["$(inherited)"] + configFrameworkBuildPaths - } - } + copyFrameworksReferences += getBuildFilesForPhase(.frameworks) + if !copyFrameworksReferences.isEmpty { - var baseConfiguration: PBXFileReference? - if let configPath = target.configFiles[config.name], - let fileReference = sourceGenerator.getContainedFileReference(path: project.basePath + configPath) as? PBXFileReference { - baseConfiguration = fileReference - } - let buildConfig = XCBuildConfiguration( - name: config.name, - buildSettings: buildSettings - ) - buildConfig.baseConfiguration = baseConfiguration - return addObject(buildConfig) - } + let copyFilesPhase = addObject( + getPBXCopyFilesBuildPhase( + dstSubfolderSpec: .frameworks, name: "Embed Frameworks", files: copyFrameworksReferences) + ) - let defaultConfigurationName = project.options.defaultConfig ?? project.configs.first?.name ?? "" - let buildConfigList = addObject(XCConfigurationList( - buildConfigurations: configs, - defaultConfigurationName: defaultConfigurationName - )) + buildPhases.append(copyFilesPhase) + } - let targetObject = targetObjects[target.name]! + if !customCopyDependenciesReferences.isEmpty { - let targetFileReference = targetFileReferences[target.name] + let splitted = splitCopyDepsByDestination(customCopyDependenciesReferences) + for (phase, references) in splitted { - targetObject.name = target.name - targetObject.buildConfigurationList = buildConfigList - targetObject.buildPhases = buildPhases - targetObject.dependencies = dependencies - targetObject.productName = target.name - targetObject.buildRules = buildRules - targetObject.packageProductDependencies = packageDependencies - targetObject.product = targetFileReference - if !target.isLegacy { - targetObject.productType = target.type - } - } - - private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? { - switch filter { - case .all: - return nil - case .macOS: - return "maccatalyst" - case .iOS: - return "ios" - } - } - - private func makeDestinationFilters(for filters: [SupportedDestination]?) -> [String]? { - guard let filters = filters, !filters.isEmpty else { return nil } - return filters.map { $0.string } - } - - /// Make `Build Tools Plug-ins` as a dependency to the target - /// - Parameter target: ProjectTarget - /// - Returns: Elements for referencing other targets through content proxies. - func makePackagePluginDependency(for target: ProjectTarget) -> [PBXTargetDependency] { - target.buildToolPlugins.compactMap { buildToolPlugin in - let packageReference = packageReferences[buildToolPlugin.package] - if packageReference == nil, localPackageReferences[buildToolPlugin.package] == nil { - return nil - } + guard let destination = phase.destination.destination else { continue } - let packageDependency = addObject( - XCSwiftPackageProductDependency(productName: buildToolPlugin.plugin, package: packageReference, isPlugin: true) - ) - let targetDependency = addObject( - PBXTargetDependency(product: packageDependency) - ) + let copyFilesPhase = addObject( + getPBXCopyFilesBuildPhase( + dstSubfolderSpec: destination, dstPath: phase.subpath, name: "Embed Dependencies", + files: references) + ) - return targetDependency - } + buildPhases.append(copyFilesPhase) + } } - - func getInfoPlists(for target: Target) -> [Config: String] { - var searchForDefaultInfoPlist: Bool = true - var defaultInfoPlist: String? - let values: [(Config, String)] = project.configs.compactMap { config in - // First, if the plist path was defined by `INFOPLIST_FILE`, use that - let buildSettings = project.getTargetBuildSettings(target: target, config: config) - if let value = buildSettings["INFOPLIST_FILE"] as? String { - return (config, value) - } - - // Otherwise check if the path was defined as part of the `info` spec - if let value = target.info?.path { - return (config, value) - } + if !copyWatchReferences.isEmpty { - // If we haven't yet looked for the default info plist, try doing so - if searchForDefaultInfoPlist { - searchForDefaultInfoPlist = false + let copyFilesPhase = addObject( + PBXCopyFilesBuildPhase( + dstPath: "$(CONTENTS_FOLDER_PATH)/Watch", + dstSubfolderSpec: .productsDirectory, + name: "Embed Watch Content", + files: copyWatchReferences + ) + ) - if let plistPath = getInfoPlist(target.sources) { - let basePath = projectDirectory ?? project.basePath.absolute() - let relative = (try? plistPath.relativePath(from: basePath)) ?? plistPath - defaultInfoPlist = relative.string - } - } + buildPhases.append(copyFilesPhase) + } - // Return the default plist if there was one - if let value = defaultInfoPlist { - return (config, value) - } - return nil - } + let buildRules = target.buildRules.map { buildRule in + addObject( + PBXBuildRule( + compilerSpec: buildRule.action.compilerSpec, + fileType: buildRule.fileType.fileType, + isEditable: true, + filePatterns: buildRule.fileType.pattern, + name: buildRule.name ?? "Build Rule", + outputFiles: buildRule.outputFiles, + outputFilesCompilerFlags: buildRule.outputFilesCompilerFlags, + script: buildRule.action.script, + runOncePerArchitecture: buildRule.runOncePerArchitecture + ) + ) + } - return Dictionary(uniqueKeysWithValues: values) + buildPhases += try target.postBuildScripts.map { + try generateBuildScript(targetName: target.name, buildScript: $0) } - func getInfoPlist(_ sources: [TargetSource]) -> Path? { - sources - .lazy - .map { self.project.basePath + $0.path } - .compactMap { (path) -> Path? in - if path.isFile { - return path.lastComponent == "Info.plist" ? path : nil - } else { - return path.first(where: { $0.lastComponent == "Info.plist" })?.absolute() - } - } - .first - } - - func getAllDependenciesPlusTransitiveNeedingEmbedding(target topLevelTarget: Target) -> [Dependency] { - // this is used to resolve cyclical target dependencies - var visitedTargets: Set = [] - var dependencies: [String: Dependency] = [:] - var queue: [Target] = [topLevelTarget] - while !queue.isEmpty { - let target = queue.removeFirst() - if visitedTargets.contains(target.name) { - continue + let configs: [XCBuildConfiguration] = project.configs.map { config in + var buildSettings = project.getTargetBuildSettings(target: target, config: config) + + // Set CODE_SIGN_ENTITLEMENTS + if let entitlements = target.entitlements { + buildSettings["CODE_SIGN_ENTITLEMENTS"] = entitlements.path + } + + // Set INFOPLIST_FILE based on the resolved value + if let infoPlistFile = infoPlistFiles[config] { + buildSettings["INFOPLIST_FILE"] = infoPlistFile + } + + // automatically calculate bundle id + if let bundleIdPrefix = project.options.bundleIdPrefix, + !project.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", target: target, config: config) + { + let characterSet = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")) + .inverted + let escapedTargetName = target.name + .replacingOccurrences(of: "_", with: "-") + .components(separatedBy: characterSet) + .joined(separator: "") + buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = bundleIdPrefix + "." + escapedTargetName + } + + // automatically set test target name + if target.type == .uiTestBundle, + !project.targetHasBuildSetting("TEST_TARGET_NAME", target: target, config: config) + { + for dependency in target.dependencies { + if dependency.type == .target, + let dependencyTarget = project.getTarget(dependency.reference), + dependencyTarget.type.isApp + { + buildSettings["TEST_TARGET_NAME"] = dependencyTarget.name + break + } + } + } + + // automatically set TEST_HOST + if target.type == .unitTestBundle, + !project.targetHasBuildSetting("TEST_HOST", target: target, config: config) + { + for dependency in target.dependencies { + if dependency.type == .target, + let dependencyTarget = project.getTarget(dependency.reference), + dependencyTarget.type.isApp + { + if dependencyTarget.platform == .macOS { + buildSettings["TEST_HOST"] = + "$(BUILT_PRODUCTS_DIR)/\(dependencyTarget.productName).app/Contents/MacOS/\(dependencyTarget.productName)" + } else { + buildSettings["TEST_HOST"] = + "$(BUILT_PRODUCTS_DIR)/\(dependencyTarget.productName).app/\(dependencyTarget.productName)" } + break + } + } + } + + // objc linkage + if anyDependencyRequiresObjCLinking { + let otherLinkingFlags = "OTHER_LDFLAGS" + let objCLinking = "-ObjC" + if var array = buildSettings[otherLinkingFlags] as? [String] { + array.append(objCLinking) + buildSettings[otherLinkingFlags] = array + } else if let string = buildSettings[otherLinkingFlags] as? String { + buildSettings[otherLinkingFlags] = [string, objCLinking] + } else { + buildSettings[otherLinkingFlags] = ["$(inherited)", objCLinking] + } + } + + // set Carthage search paths + let configFrameworkBuildPaths: [String] + if !carthageDependencies.isEmpty { + var carthagePlatformBuildPaths: Set = [] + if carthageDependencies.contains(where: { $0.dependency.carthageLinkType == .static }) { + let carthagePlatformBuildPath = + "$(PROJECT_DIR)/" + carthageResolver.buildPath(for: target.platform, linkType: .static) + carthagePlatformBuildPaths.insert(carthagePlatformBuildPath) + } + if carthageDependencies.contains(where: { $0.dependency.carthageLinkType == .dynamic }) { + let carthagePlatformBuildPath = + "$(PROJECT_DIR)/" + carthageResolver.buildPath(for: target.platform, linkType: .dynamic) + carthagePlatformBuildPaths.insert(carthagePlatformBuildPath) + } + configFrameworkBuildPaths = + carthagePlatformBuildPaths.sorted() + frameworkBuildPaths.sorted() + } else { + configFrameworkBuildPaths = frameworkBuildPaths.sorted() + } + + // set framework search paths + if !configFrameworkBuildPaths.isEmpty { + let frameworkSearchPaths = "FRAMEWORK_SEARCH_PATHS" + if var array = buildSettings[frameworkSearchPaths] as? [String] { + array.append(contentsOf: configFrameworkBuildPaths) + buildSettings[frameworkSearchPaths] = array + } else if let string = buildSettings[frameworkSearchPaths] as? String { + buildSettings[frameworkSearchPaths] = [string] + configFrameworkBuildPaths + } else { + buildSettings[frameworkSearchPaths] = ["$(inherited)"] + configFrameworkBuildPaths + } + } + + var baseConfiguration: PBXFileReference? + if let configPath = target.configFiles[config.name], + let fileReference = sourceGenerator.getContainedFileReference( + path: project.basePath + configPath) as? PBXFileReference + { + baseConfiguration = fileReference + } + let buildConfig = XCBuildConfiguration( + name: config.name, + buildSettings: buildSettings + ) + buildConfig.baseConfiguration = baseConfiguration + return addObject(buildConfig) + } - let isTopLevel = target == topLevelTarget + let defaultConfigurationName = + project.options.defaultConfig ?? project.configs.first?.name ?? "" + let buildConfigList = addObject( + XCConfigurationList( + buildConfigurations: configs, + defaultConfigurationName: defaultConfigurationName + )) + + let targetObject = targetObjects[target.name]! + + let targetFileReference = targetFileReferences[target.name] + + targetObject.name = target.name + targetObject.buildConfigurationList = buildConfigList + targetObject.buildPhases = buildPhases + targetObject.dependencies = dependencies + targetObject.productName = target.name + targetObject.buildRules = buildRules + targetObject.packageProductDependencies = packageDependencies + targetObject.product = targetFileReference + if !target.isLegacy { + targetObject.productType = target.type + } + } + + private func makePlatformFilter(for filter: Dependency.PlatformFilter) -> String? { + switch filter { + case .all: + return nil + case .macOS: + return "maccatalyst" + case .iOS: + return "ios" + } + } + + private func makeDestinationFilters(for filters: [SupportedDestination]?) -> [String]? { + guard let filters = filters, !filters.isEmpty else { return nil } + return filters.map { $0.string } + } + + /// Make `Build Tools Plug-ins` as a dependency to the target + /// - Parameter target: ProjectTarget + /// - Returns: Elements for referencing other targets through content proxies. + func makePackagePluginDependency(for target: ProjectTarget) -> [PBXTargetDependency] { + target.buildToolPlugins.compactMap { buildToolPlugin in + let packageReference = packageReferences[buildToolPlugin.package] + if packageReference == nil, localPackageReferences[buildToolPlugin.package] == nil { + return nil + } + + let packageDependency = addObject( + XCSwiftPackageProductDependency( + productName: buildToolPlugin.plugin, package: packageReference, isPlugin: true) + ) + let targetDependency = addObject( + PBXTargetDependency(product: packageDependency) + ) + + return targetDependency + } + } + + func getInfoPlists(for target: Target) -> [Config: String] { + var searchForDefaultInfoPlist: Bool = true + var defaultInfoPlist: String? + + let values: [(Config, String)] = project.configs.compactMap { config in + // First, if the plist path was defined by `INFOPLIST_FILE`, use that + let buildSettings = project.getTargetBuildSettings(target: target, config: config) + if let value = buildSettings["INFOPLIST_FILE"] as? String { + return (config, value) + } + + // Otherwise check if the path was defined as part of the `info` spec + if let value = target.info?.path { + return (config, value) + } + + // If we haven't yet looked for the default info plist, try doing so + if searchForDefaultInfoPlist { + searchForDefaultInfoPlist = false + + if let plistPath = getInfoPlist(target.sources) { + let basePath = projectDirectory ?? project.basePath.absolute() + let relative = (try? plistPath.relativePath(from: basePath)) ?? plistPath + defaultInfoPlist = relative.string + } + } + + // Return the default plist if there was one + if let value = defaultInfoPlist { + return (config, value) + } + return nil + } - for dependency in target.dependencies { - // don't overwrite dependencies, to allow top level ones to rule - if dependencies[dependency.uniqueID] != nil { - continue - } + return Dictionary(uniqueKeysWithValues: values) + } - // don't want a dependency if it's going to be embedded or statically linked in a non-top level target - // in .target check we filter out targets that will embed all of their dependencies - // For some more context about the `dependency.embed != true` lines, refer to https://github.com/yonaskolb/XcodeGen/pull/820 - switch dependency.type { - case .sdk: - dependencies[dependency.uniqueID] = dependency - case .framework, .carthage, .package: - if isTopLevel || dependency.embed != true { - dependencies[dependency.uniqueID] = dependency - } - case .target: - let dependencyTargetReference = try! TargetReference(dependency.reference) - - switch dependencyTargetReference.location { - case .local: - if isTopLevel || dependency.embed != true { - if let dependencyTarget = project.getTarget(dependency.reference) { - dependencies[dependency.uniqueID] = dependency - if !dependencyTarget.shouldEmbedDependencies { - // traverse target's dependencies if it doesn't embed them itself - queue.append(dependencyTarget) - } - } else if project.getAggregateTarget(dependency.reference) != nil { - // Aggregate targets should be included - dependencies[dependency.uniqueID] = dependency - } - } - case .project: - if isTopLevel || dependency.embed != true { - dependencies[dependency.uniqueID] = dependency - } - } - case .bundle: - if isTopLevel { - dependencies[dependency.uniqueID] = dependency - } + func getInfoPlist(_ sources: [TargetSource]) -> Path? { + sources + .lazy + .map { self.project.basePath + $0.path } + .compactMap { (path) -> Path? in + if path.isFile { + return path.lastComponent == "Info.plist" ? path : nil + } else { + return path.first(where: { $0.lastComponent == "Info.plist" })?.absolute() + } + } + .first + } + + func getAllDependenciesPlusTransitiveNeedingEmbedding(target topLevelTarget: Target) + -> [Dependency] + { + // this is used to resolve cyclical target dependencies + var visitedTargets: Set = [] + var dependencies: [String: Dependency] = [:] + var queue: [Target] = [topLevelTarget] + while !queue.isEmpty { + let target = queue.removeFirst() + if visitedTargets.contains(target.name) { + continue + } + + let isTopLevel = target == topLevelTarget + + for dependency in target.dependencies { + // don't overwrite dependencies, to allow top level ones to rule + if dependencies[dependency.uniqueID] != nil { + continue + } + + // don't want a dependency if it's going to be embedded or statically linked in a non-top level target + // in .target check we filter out targets that will embed all of their dependencies + // For some more context about the `dependency.embed != true` lines, refer to https://github.com/yonaskolb/XcodeGen/pull/820 + switch dependency.type { + case .sdk: + dependencies[dependency.uniqueID] = dependency + case .framework, .carthage, .package: + if isTopLevel || dependency.embed != true { + dependencies[dependency.uniqueID] = dependency + } + case .target: + let dependencyTargetReference = try! TargetReference(dependency.reference) + + switch dependencyTargetReference.location { + case .local: + if isTopLevel || dependency.embed != true { + if let dependencyTarget = project.getTarget(dependency.reference) { + dependencies[dependency.uniqueID] = dependency + if !dependencyTarget.shouldEmbedDependencies { + // traverse target's dependencies if it doesn't embed them itself + queue.append(dependencyTarget) } + } else if project.getAggregateTarget(dependency.reference) != nil { + // Aggregate targets should be included + dependencies[dependency.uniqueID] = dependency + } } - - visitedTargets.update(with: target.name) + case .project: + if isTopLevel || dependency.embed != true { + dependencies[dependency.uniqueID] = dependency + } + } + case .bundle: + if isTopLevel { + dependencies[dependency.uniqueID] = dependency + } } + } - return dependencies.sorted(by: { $0.key < $1.key }).map { $0.value } + visitedTargets.update(with: target.name) } + + return dependencies.sorted(by: { $0.key < $1.key }).map { $0.value } + } } extension Target { - var shouldEmbedDependencies: Bool { - type.isApp || type.isTest - } + var shouldEmbedDependencies: Bool { + type.isApp || type.isTest + } - var shouldEmbedCarthageDependencies: Bool { - (type.isApp && platform != .watchOS) - || type == .watch2Extension - || type.isTest - } + var shouldEmbedCarthageDependencies: Bool { + (type.isApp && platform != .watchOS) + || type == .watch2Extension + || type.isTest + } } extension Platform { - /// - returns: `true` for platforms that the app store requires simulator slices to be stripped. - public var requiresSimulatorStripping: Bool { - switch self { - case .auto, .iOS, .tvOS, .watchOS, .visionOS: - return true - case .macOS: - return false - } + /// - returns: `true` for platforms that the app store requires simulator slices to be stripped. + public var requiresSimulatorStripping: Bool { + switch self { + case .auto, .iOS, .tvOS, .watchOS, .visionOS: + return true + case .macOS: + return false } + } } extension PBXFileElement { - /// - returns: `true` if the element is a group or a folder reference. Likely an SPM package. - var isGroupOrFolder: Bool { - self is PBXGroup || (self as? PBXFileReference)?.lastKnownFileType == "folder" - } - - public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int { - if type(of: self).isa == "PBXGroup" { - switch groupSortPosition { - case .top: return -1 - case .bottom: return 1 - case .none: return 0 - } - } else { - return 0 - } + /// - returns: `true` if the element is a group or a folder reference. Likely an SPM package. + var isGroupOrFolder: Bool { + self is PBXGroup || (self as? PBXFileReference)?.lastKnownFileType == "folder" + } + + public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int { + if type(of: self).isa == "PBXGroup" { + switch groupSortPosition { + case .top: return -1 + case .bottom: return 1 + case .none: return 0 + } + } else { + return 0 } + } } -private extension Dependency { - var carthageLinkType: Dependency.CarthageLinkType? { - switch type { - case .carthage(_, let linkType): - return linkType - default: - return nil - } +extension Dependency { + fileprivate var carthageLinkType: Dependency.CarthageLinkType? { + switch type { + case .carthage(_, let linkType): + return linkType + default: + return nil } + } } diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 1ce4d7d8d..0abe95e51 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -1,848 +1,1013 @@ import Foundation import PathKit import ProjectSpec -import XcodeProj import XcodeGenCore +import XcodeProj struct SourceFile { - let path: Path - let fileReference: PBXFileElement - let buildFile: PBXBuildFile - let buildPhase: BuildPhaseSpec? + let path: Path + let fileReference: PBXFileElement + let buildFile: PBXBuildFile + let buildPhase: BuildPhaseSpec? } class SourceGenerator { - var rootGroups: Set = [] - private let projectDirectory: Path? - private var fileReferencesByPath: [String: PBXFileElement] = [:] - private var groupsByPath: [Path: PBXGroup] = [:] - private var variantGroupsByPath: [Path: PBXVariantGroup] = [:] - - private let project: Project - let pbxProj: PBXProj - - var excludePatterns: [NSRegularExpression] = [] - private var defaultExcludedFiles = [ - ".DS_Store", - ] - private let defaultExcludedExtensions = [ - "orig", - ] - - private(set) var knownRegions: Set = [] - - init(project: Project, pbxProj: PBXProj, projectDirectory: Path?) { - self.project = project - self.pbxProj = pbxProj - self.projectDirectory = projectDirectory + var rootGroups: Set = [] + private let projectDirectory: Path? + private var fileReferencesByPath: [String: PBXFileElement] = [:] + private var groupsByPath: [Path: PBXGroup] = [:] + private var variantGroupsByPath: [Path: PBXVariantGroup] = [:] + + private let project: Project + let pbxProj: PBXProj + + var excludePatterns: [NSRegularExpression] = [] + private var defaultExcludedFiles = [ + ".DS_Store" + ] + private let defaultExcludedExtensions = [ + "orig" + ] + + private(set) var knownRegions: Set = [] + + init(project: Project, pbxProj: PBXProj, projectDirectory: Path?) { + self.project = project + self.pbxProj = pbxProj + self.projectDirectory = projectDirectory + } + + private func resolveGroupPath(_ path: Path, isTopLevelGroup: Bool) -> String { + if isTopLevelGroup, + let relativePath = try? path.relativePath(from: projectDirectory ?? project.basePath).string + { + return relativePath + } else { + return path.lastComponent } - - private func resolveGroupPath(_ path: Path, isTopLevelGroup: Bool) -> String { - if isTopLevelGroup, let relativePath = try? path.relativePath(from: projectDirectory ?? project.basePath).string { - return relativePath - } else { - return path.lastComponent - } + } + + @discardableResult + func addObject(_ object: T, context: String? = nil) -> T { + pbxProj.add(object: object) + object.context = context + return object + } + + func createLocalPackage(path: Path, group: Path?) throws { + var parentGroup: String = project.options.localPackagesGroup ?? "Packages" + if let group { + parentGroup = group.string } - @discardableResult - func addObject(_ object: T, context: String? = nil) -> T { - pbxProj.add(object: object) - object.context = context - return object + let absolutePath = project.basePath + path.normalize() + + // Get the local package's relative path from the project root + let fileReferencePath = try? absolutePath.relativePath( + from: projectDirectory ?? project.basePath + ).string + + let fileReference = addObject( + PBXFileReference( + sourceTree: .sourceRoot, + name: absolutePath.lastComponent, + lastKnownFileType: "folder", + path: fileReferencePath + ) + ) + + if parentGroup == "" { + rootGroups.insert(fileReference) + } else { + let parentGroups = parentGroup.components(separatedBy: "/") + createParentGroups(parentGroups, for: fileReference) } - - func createLocalPackage(path: Path, group: Path?) throws { - var parentGroup: String = project.options.localPackagesGroup ?? "Packages" - if let group { - parentGroup = group.string - } - - let absolutePath = project.basePath + path.normalize() - - // Get the local package's relative path from the project root - let fileReferencePath = try? absolutePath.relativePath(from: projectDirectory ?? project.basePath).string - - let fileReference = addObject( - PBXFileReference( - sourceTree: .sourceRoot, - name: absolutePath.lastComponent, - lastKnownFileType: "folder", - path: fileReferencePath - ) + } + + /// Collects an array complete of all `SourceFile` objects that make up the target based on the provided `TargetSource` definitions. + /// + /// - Parameters: + /// - targetType: The type of target that the source files should belong to. + /// - sources: The array of sources defined as part of the targets spec. + /// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on). + func getAllSourceFiles( + targetName: String, targetType: PBXProductType, sources: [TargetSource], platform: Platform, + buildPhases: [Path: BuildPhaseSpec] + ) throws -> [SourceFile] { + try sources + .flatMap { + try getSourceFiles( + targetName: targetName, + targetType: targetType, + targetSource: $0, + platform: platform, + buildPhases: buildPhases ) - - if parentGroup == "" { - rootGroups.insert(fileReference) - } else { - let parentGroups = parentGroup.components(separatedBy: "/") - createParentGroups(parentGroups, for: fileReference) - } + } + } + + // get groups without build files. Use for Project.fileGroups + func getFileGroups(path: String) throws { + _ = try getSourceFiles( + targetName: "", targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:]) + } + + func getFileType(path: Path) -> FileType? { + if let fileExtension = path.extension { + return project.options.fileTypes[fileExtension] ?? FileType.defaultFileTypes[fileExtension] + } else { + return nil } - - /// Collects an array complete of all `SourceFile` objects that make up the target based on the provided `TargetSource` definitions. - /// - /// - Parameters: - /// - targetType: The type of target that the source files should belong to. - /// - sources: The array of sources defined as part of the targets spec. - /// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on). - func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], platform: Platform, buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] { - try sources - .flatMap { - try getSourceFiles( - targetType: targetType, - targetSource: $0, - platform: platform, - buildPhases: buildPhases - ) - } + } + + private func makeDestinationFilters( + for path: Path, with filters: [SupportedDestination]?, or inferDestinationFiltersByPath: Bool? + ) -> [String]? { + if let filters = filters, !filters.isEmpty { + return filters.map { $0.string } + } else if inferDestinationFiltersByPath == true { + for supportedDestination in SupportedDestination.allCases { + let regex1 = try? NSRegularExpression( + pattern: "\\/\(supportedDestination)\\/", options: .caseInsensitive) + let regex2 = try? NSRegularExpression( + pattern: "\\_\(supportedDestination)\\.swift$", options: .caseInsensitive) + + if regex1?.isMatch(to: path.string) == true || regex2?.isMatch(to: path.string) == true { + return [supportedDestination.string] + } + } } - - // get groups without build files. Use for Project.fileGroups - func getFileGroups(path: String) throws { - _ = try getSourceFiles(targetType: .none, targetSource: TargetSource(path: path), buildPhases: [:]) + return nil + } + + func generateSourceFile( + targetType: PBXProductType, targetSource: TargetSource, path: Path, + fileReference: PBXFileElement? = nil, buildPhases: [Path: BuildPhaseSpec] + ) -> SourceFile { + let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]! + var settings: [String: Any] = [:] + let fileType = getFileType(path: path) + var attributes: [String] = targetSource.attributes + (fileType?.attributes ?? []) + var chosenBuildPhase: BuildPhaseSpec? + var compilerFlags: String = "" + let assetTags: [String] = targetSource.resourceTags + (fileType?.resourceTags ?? []) + + let headerVisibility = targetSource.headerVisibility ?? .public + + if let buildPhase = targetSource.buildPhase { + chosenBuildPhase = buildPhase + } else if resolvedTargetSourceType(for: targetSource, at: path) == .folder { + chosenBuildPhase = .resources + } else if let buildPhase = buildPhases[path] { + chosenBuildPhase = buildPhase + } else { + chosenBuildPhase = getDefaultBuildPhase(for: path, targetType: targetType) } - func getFileType(path: Path) -> FileType? { - if let fileExtension = path.extension { - return project.options.fileTypes[fileExtension] ?? FileType.defaultFileTypes[fileExtension] - } else { - return nil - } + if chosenBuildPhase == .headers && targetType == .staticLibrary { + // Static libraries don't support the header build phase + // For public headers they need to be copied + if headerVisibility == .public { + chosenBuildPhase = .copyFiles( + BuildPhaseSpec.CopyFilesSettings( + destination: .productsDirectory, + subpath: "include/$(PRODUCT_NAME)", + phaseOrder: .preCompile + )) + } else { + chosenBuildPhase = nil + } } - - private func makeDestinationFilters(for path: Path, with filters: [SupportedDestination]?, or inferDestinationFiltersByPath: Bool?) -> [String]? { - if let filters = filters, !filters.isEmpty { - return filters.map { $0.string } - } else if inferDestinationFiltersByPath == true { - for supportedDestination in SupportedDestination.allCases { - let regex1 = try? NSRegularExpression(pattern: "\\/\(supportedDestination)\\/", options: .caseInsensitive) - let regex2 = try? NSRegularExpression(pattern: "\\_\(supportedDestination)\\.swift$", options: .caseInsensitive) - - if regex1?.isMatch(to: path.string) == true || regex2?.isMatch(to: path.string) == true { - return [supportedDestination.string] - } - } - } - return nil - } - - func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, fileReference: PBXFileElement? = nil, buildPhases: [Path: BuildPhaseSpec]) -> SourceFile { - let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]! - var settings: [String: Any] = [:] - let fileType = getFileType(path: path) - var attributes: [String] = targetSource.attributes + (fileType?.attributes ?? []) - var chosenBuildPhase: BuildPhaseSpec? - var compilerFlags: String = "" - let assetTags: [String] = targetSource.resourceTags + (fileType?.resourceTags ?? []) - - let headerVisibility = targetSource.headerVisibility ?? .public - - if let buildPhase = targetSource.buildPhase { - chosenBuildPhase = buildPhase - } else if resolvedTargetSourceType(for: targetSource, at: path) == .folder { - chosenBuildPhase = .resources - } else if let buildPhase = buildPhases[path] { - chosenBuildPhase = buildPhase - } else { - chosenBuildPhase = getDefaultBuildPhase(for: path, targetType: targetType) - } - - if chosenBuildPhase == .headers && targetType == .staticLibrary { - // Static libraries don't support the header build phase - // For public headers they need to be copied - if headerVisibility == .public { - chosenBuildPhase = .copyFiles(BuildPhaseSpec.CopyFilesSettings( - destination: .productsDirectory, - subpath: "include/$(PRODUCT_NAME)", - phaseOrder: .preCompile - )) - } else { - chosenBuildPhase = nil - } - } - - if chosenBuildPhase == .headers { - if headerVisibility != .project { - // Xcode doesn't write the default of project - attributes.append(headerVisibility.settingName) - } - } - - if let flags = fileType?.compilerFlags { - compilerFlags += flags.joined(separator: " ") - } - - if !targetSource.compilerFlags.isEmpty { - if !compilerFlags.isEmpty { - compilerFlags += " " - } - compilerFlags += targetSource.compilerFlags.joined(separator: " ") - } - if chosenBuildPhase == .sources && !compilerFlags.isEmpty { - settings["COMPILER_FLAGS"] = compilerFlags - } - - if !attributes.isEmpty { - settings["ATTRIBUTES"] = attributes - } - - if chosenBuildPhase == .resources && !assetTags.isEmpty { - settings["ASSET_TAGS"] = assetTags - } - - let platforms = makeDestinationFilters(for: path, with: targetSource.destinationFilters, or: targetSource.inferDestinationFiltersByPath) - - let buildFile = PBXBuildFile(file: fileReference, settings: settings.isEmpty ? nil : settings, platformFilters: platforms) - return SourceFile( - path: path, - fileReference: fileReference, - buildFile: buildFile, - buildPhase: chosenBuildPhase - ) + if chosenBuildPhase == .headers { + if headerVisibility != .project { + // Xcode doesn't write the default of project + attributes.append(headerVisibility.settingName) + } } - func getContainedFileReference(path: Path) -> PBXFileElement { - let createIntermediateGroups = project.options.createIntermediateGroups - - let parentPath = path.parent() - let fileReference = getFileReference(path: path, inPath: parentPath) - let parentGroup = getGroup( - path: parentPath, - mergingChildren: [fileReference], - createIntermediateGroups: createIntermediateGroups, - hasCustomParent: false, - isBaseGroup: true - ) - - if createIntermediateGroups { - createIntermediaGroups(for: parentGroup, at: parentPath) - } - return fileReference + if let flags = fileType?.compilerFlags { + compilerFlags += flags.joined(separator: " ") } - func getFileReference(path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group, lastKnownFileType: String? = nil) -> PBXFileElement { - let fileReferenceKey = path.string.lowercased() - if let fileReference = fileReferencesByPath[fileReferenceKey] { - return fileReference - } else { - let fileReferencePath = (try? path.relativePath(from: inPath)) ?? path - var fileReferenceName: String? = name ?? fileReferencePath.lastComponent - if fileReferencePath.string == fileReferenceName { - fileReferenceName = nil - } - let lastKnownFileType = lastKnownFileType ?? Xcode.fileType(path: path) - - if path.extension == "xcdatamodeld" { - let versionedModels = (try? path.children()) ?? [] - - // Sort the versions alphabetically - let sortedPaths = versionedModels - .filter { $0.extension == "xcdatamodel" } - .sorted { $0.string.localizedStandardCompare($1.string) == .orderedAscending } - - let modelFileReferences = - sortedPaths.map { path in - addObject( - PBXFileReference( - sourceTree: .group, - lastKnownFileType: "wrapper.xcdatamodel", - path: path.lastComponent - ) - ) - } - // If no current version path is found we fall back to alphabetical - // order by taking the last item in the sortedPaths array - let currentVersionPath = findCurrentCoreDataModelVersionPath(using: versionedModels) ?? sortedPaths.last - let currentVersion: PBXFileReference? = { - guard let indexOf = sortedPaths.firstIndex(where: { $0 == currentVersionPath }) else { return nil } - return modelFileReferences[indexOf] - }() - let versionGroup = addObject(XCVersionGroup( - currentVersion: currentVersion, - path: fileReferencePath.string, - sourceTree: sourceTree, - versionGroupType: "wrapper.xcdatamodel", - children: modelFileReferences - )) - fileReferencesByPath[fileReferenceKey] = versionGroup - return versionGroup - } else { - // For all extensions other than `xcdatamodeld` - let fileReference = addObject( - PBXFileReference( - sourceTree: sourceTree, - name: fileReferenceName, - lastKnownFileType: lastKnownFileType, - path: fileReferencePath.string - ) - ) - fileReferencesByPath[fileReferenceKey] = fileReference - return fileReference - } - } + if !targetSource.compilerFlags.isEmpty { + if !compilerFlags.isEmpty { + compilerFlags += " " + } + compilerFlags += targetSource.compilerFlags.joined(separator: " ") } - /// returns a default build phase for a given path. This is based off the filename - private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> BuildPhaseSpec? { - if let buildPhase = getFileType(path: path)?.buildPhase { - return buildPhase - } - if let fileExtension = path.extension { - switch fileExtension { - case "modulemap": - guard targetType == .staticLibrary else { return nil } - return .copyFiles(BuildPhaseSpec.CopyFilesSettings( - destination: .productsDirectory, - subpath: "include/$(PRODUCT_NAME)", - phaseOrder: .preCompile - )) - case "swiftcrossimport": - guard targetType == .framework else { return nil } - return .copyFiles(BuildPhaseSpec.CopyFilesSettings( - destination: .productsDirectory, - subpath: "$(PRODUCT_NAME).framework/Modules", - phaseOrder: .preCompile - )) - default: - return .resources - } - } - return nil - } - - /// Create a group or return an existing one at the path. - /// Any merged children are added to a new group or merged into an existing one. - private func getGroup(path: Path, name: String? = nil, mergingChildren children: [PBXFileElement], createIntermediateGroups: Bool, hasCustomParent: Bool, isBaseGroup: Bool) -> PBXGroup { - let groupReference: PBXGroup - - if let cachedGroup = groupsByPath[path] { - var cachedGroupChildren = cachedGroup.children - for child in children { - // only add the children that aren't already in the cachedGroup - // Check equality by path and sourceTree because XcodeProj.PBXObject.== is very slow. - if !cachedGroupChildren.contains(where: { $0.name == child.name && $0.path == child.path && $0.sourceTree == child.sourceTree }) { - cachedGroupChildren.append(child) - child.parent = cachedGroup - } - } - cachedGroup.children = cachedGroupChildren - groupReference = cachedGroup - } else { - - // lives outside the project base path - let isOutOfBasePath = !path.absolute().string.contains(project.basePath.absolute().string) - - // whether the given path is a strict parent of the project base path - // e.g. foo/bar is a parent of foo/bar/baz, but not foo/baz - let isParentOfBasePath = isOutOfBasePath && ((try? path.isParent(of: project.basePath)) == true) - - // has no valid parent paths - let isRootPath = (isBaseGroup && isOutOfBasePath && isParentOfBasePath) || path.parent() == project.basePath - - // is a top level group in the project - let isTopLevelGroup = !hasCustomParent && ((isBaseGroup && !createIntermediateGroups) || isRootPath || isParentOfBasePath) - - let groupName = name ?? path.lastComponent - - let groupPath = resolveGroupPath(path, isTopLevelGroup: hasCustomParent || isTopLevelGroup) + if chosenBuildPhase == .sources && !compilerFlags.isEmpty { + settings["COMPILER_FLAGS"] = compilerFlags + } - let group = PBXGroup( - children: children, - sourceTree: .group, - name: groupName != groupPath ? groupName : nil, - path: groupPath - ) - groupReference = addObject(group) - groupsByPath[path] = groupReference + if !attributes.isEmpty { + settings["ATTRIBUTES"] = attributes + } - if isTopLevelGroup { - rootGroups.insert(groupReference) - } - } - return groupReference + if chosenBuildPhase == .resources && !assetTags.isEmpty { + settings["ASSET_TAGS"] = assetTags } - /// Creates a variant group or returns an existing one at the path - private func getVariantGroup(path: Path, inPath: Path) -> PBXVariantGroup { - let variantGroup: PBXVariantGroup - if let cachedGroup = variantGroupsByPath[path] { - variantGroup = cachedGroup - } else { - let group = PBXVariantGroup( + let platforms = makeDestinationFilters( + for: path, with: targetSource.destinationFilters, + or: targetSource.inferDestinationFiltersByPath) + + let buildFile = PBXBuildFile( + file: fileReference, settings: settings.isEmpty ? nil : settings, platformFilters: platforms) + return SourceFile( + path: path, + fileReference: fileReference, + buildFile: buildFile, + buildPhase: chosenBuildPhase + ) + } + + func getContainedFileReference(path: Path) -> PBXFileElement { + let createIntermediateGroups = project.options.createIntermediateGroups + + let parentPath = path.parent() + let fileReference = getFileReference(path: path, inPath: parentPath) + let parentGroup = getGroup( + path: parentPath, + mergingChildren: [fileReference], + createIntermediateGroups: createIntermediateGroups, + hasCustomParent: false, + isBaseGroup: true + ) + + if createIntermediateGroups { + createIntermediaGroups(for: parentGroup, at: parentPath) + } + return fileReference + } + + func getFileReference( + path: Path, inPath: Path, name: String? = nil, sourceTree: PBXSourceTree = .group, + lastKnownFileType: String? = nil + ) -> PBXFileElement { + let fileReferenceKey = path.string.lowercased() + if let fileReference = fileReferencesByPath[fileReferenceKey] { + return fileReference + } else { + let fileReferencePath = (try? path.relativePath(from: inPath)) ?? path + var fileReferenceName: String? = name ?? fileReferencePath.lastComponent + if fileReferencePath.string == fileReferenceName { + fileReferenceName = nil + } + let lastKnownFileType = lastKnownFileType ?? Xcode.fileType(path: path) + + if path.extension == "xcdatamodeld" { + let versionedModels = (try? path.children()) ?? [] + + // Sort the versions alphabetically + let sortedPaths = + versionedModels + .filter { $0.extension == "xcdatamodel" } + .sorted { $0.string.localizedStandardCompare($1.string) == .orderedAscending } + + let modelFileReferences = + sortedPaths.map { path in + addObject( + PBXFileReference( sourceTree: .group, - name: path.lastComponent + lastKnownFileType: "wrapper.xcdatamodel", + path: path.lastComponent + ) ) - variantGroup = addObject(group) - variantGroupsByPath[path] = variantGroup - } - return variantGroup - } - - /// Collects all the excluded paths within the targetSource - private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set { - let rootSourcePath = project.basePath + targetSource.path - - return Set( - patterns.parallelMap { pattern in - guard !pattern.isEmpty else { return [] } - return Glob(pattern: "\(rootSourcePath)/\(pattern)") - .map { Path($0) } - .map { - guard $0.isDirectory else { - return [$0] - } - - return (try? $0.recursiveChildren()) ?? [] - } - .reduce([], +) - } - .reduce([], +) + } + // If no current version path is found we fall back to alphabetical + // order by taking the last item in the sortedPaths array + let currentVersionPath = + findCurrentCoreDataModelVersionPath(using: versionedModels) ?? sortedPaths.last + let currentVersion: PBXFileReference? = { + guard let indexOf = sortedPaths.firstIndex(where: { $0 == currentVersionPath }) else { + return nil + } + return modelFileReferences[indexOf] + }() + let versionGroup = addObject( + XCVersionGroup( + currentVersion: currentVersion, + path: fileReferencePath.string, + sourceTree: sourceTree, + versionGroupType: "wrapper.xcdatamodel", + children: modelFileReferences + )) + fileReferencesByPath[fileReferenceKey] = versionGroup + return versionGroup + } else { + // For all extensions other than `xcdatamodeld` + let fileReference = addObject( + PBXFileReference( + sourceTree: sourceTree, + name: fileReferenceName, + lastKnownFileType: lastKnownFileType, + path: fileReferencePath.string + ) ) + fileReferencesByPath[fileReferenceKey] = fileReference + return fileReference + } } - - func isExcludedPattern(_ path: Path) -> Bool { - return excludePatterns.reduce(false) { - (result: Bool, expression: NSRegularExpression) -> Bool in - - let string: String = path.string - let range = NSRange(location: 0, length: string.count) - let matches = expression.matches(in: string, range: range) + } - return result || (matches.count > 0) - } + /// returns a default build phase for a given path. This is based off the filename + private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> BuildPhaseSpec? { + if let buildPhase = getFileType(path: path)?.buildPhase { + return buildPhase } - - /// Checks whether the path is not in any default or TargetSource excludes - func isIncludedPath(_ path: Path, excludePaths: Set, includePaths: SortedArray?) -> Bool { - return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 }) - && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) - && !excludePaths.contains(path) - && !isExcludedPattern(path) - // If includes is empty, it's included. If it's not empty, the path either needs to match exactly, or it needs to be a direct parent of an included path. - && (includePaths.flatMap { _isIncludedPathSorted(path, sortedPaths: $0) } ?? true) + if let fileExtension = path.extension { + switch fileExtension { + case "modulemap": + guard targetType == .staticLibrary else { return nil } + return .copyFiles( + BuildPhaseSpec.CopyFilesSettings( + destination: .productsDirectory, + subpath: "include/$(PRODUCT_NAME)", + phaseOrder: .preCompile + )) + case "swiftcrossimport": + guard targetType == .framework else { return nil } + return .copyFiles( + BuildPhaseSpec.CopyFilesSettings( + destination: .productsDirectory, + subpath: "$(PRODUCT_NAME).framework/Modules", + phaseOrder: .preCompile + )) + default: + return .resources + } } - - private func _isIncludedPathSorted(_ path: Path, sortedPaths: SortedArray) -> Bool { - guard let idx = sortedPaths.firstIndex(where: { $0 >= path }) else { return false } - let foundPath = sortedPaths.value[idx] - return foundPath.description.hasPrefix(path.description) + return nil + } + + /// Create a group or return an existing one at the path. + /// Any merged children are added to a new group or merged into an existing one. + private func getGroup( + path: Path, name: String? = nil, mergingChildren children: [PBXFileElement], + createIntermediateGroups: Bool, hasCustomParent: Bool, isBaseGroup: Bool + ) -> PBXGroup { + let groupReference: PBXGroup + + if let cachedGroup = groupsByPath[path] { + var cachedGroupChildren = cachedGroup.children + for child in children { + // only add the children that aren't already in the cachedGroup + // Check equality by path and sourceTree because XcodeProj.PBXObject.== is very slow. + if !cachedGroupChildren.contains(where: { + $0.name == child.name && $0.path == child.path && $0.sourceTree == child.sourceTree + }) { + cachedGroupChildren.append(child) + child.parent = cachedGroup + } + } + cachedGroup.children = cachedGroupChildren + groupReference = cachedGroup + } else { + + // lives outside the project base path + let isOutOfBasePath = !path.absolute().string.contains(project.basePath.absolute().string) + + // whether the given path is a strict parent of the project base path + // e.g. foo/bar is a parent of foo/bar/baz, but not foo/baz + let isParentOfBasePath = + isOutOfBasePath && ((try? path.isParent(of: project.basePath)) == true) + + // has no valid parent paths + let isRootPath = + (isBaseGroup && isOutOfBasePath && isParentOfBasePath) || path.parent() == project.basePath + + // is a top level group in the project + let isTopLevelGroup = + !hasCustomParent + && ((isBaseGroup && !createIntermediateGroups) || isRootPath || isParentOfBasePath) + + let groupName = name ?? path.lastComponent + + let groupPath = resolveGroupPath(path, isTopLevelGroup: hasCustomParent || isTopLevelGroup) + + let group = PBXGroup( + children: children, + sourceTree: .group, + name: groupName != groupPath ? groupName : nil, + path: groupPath + ) + groupReference = addObject(group) + groupsByPath[path] = groupReference + + if isTopLevelGroup { + rootGroups.insert(groupReference) + } } - - - /// Gets all the children paths that aren't excluded - private func getSourceChildren(targetSource: TargetSource, dirPath: Path, excludePaths: Set, includePaths: SortedArray?) throws -> [Path] { - try dirPath.children() - .filter { - if $0.isDirectory { - let children = try $0.children() - - if children.isEmpty { - return project.options.generateEmptyDirectories - } - - return !children - .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } - .isEmpty - } else if $0.isFile { - return self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) - } else { - return false - } - } + return groupReference + } + + /// Creates a variant group or returns an existing one at the path + private func getVariantGroup(path: Path, inPath: Path) -> PBXVariantGroup { + let variantGroup: PBXVariantGroup + if let cachedGroup = variantGroupsByPath[path] { + variantGroup = cachedGroup + } else { + let group = PBXVariantGroup( + sourceTree: .group, + name: path.lastComponent + ) + variantGroup = addObject(group) + variantGroupsByPath[path] = variantGroup } - - /// creates all the source files and groups they belong to for a given targetSource - private func getGroupSources( - targetType: PBXProductType, - targetSource: TargetSource, - path: Path, - isBaseGroup: Bool, - hasCustomParent: Bool, - excludePaths: Set, - includePaths: SortedArray?, - buildPhases: [Path: BuildPhaseSpec] - ) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) { - - let children = try getSourceChildren(targetSource: targetSource, dirPath: path, excludePaths: excludePaths, includePaths: includePaths) - - let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups - let nonLocalizedChildren = children.filter { $0.extension != "lproj" } - let stringCatalogChildren = children.filter { $0.extension == "xcstrings" } - - let directories = nonLocalizedChildren - .filter { - if let fileType = getFileType(path: $0) { - return !fileType.file - } else { - return $0.isDirectory && !Xcode.isDirectoryFileWrapper(path: $0) - } - } - - let filePaths = nonLocalizedChildren - .filter { - if let fileType = getFileType(path: $0) { - return fileType.file - } else { - return $0.isFile || $0.isDirectory && Xcode.isDirectoryFileWrapper(path: $0) - } - } - - let localisedDirectories = children - .filter { $0.extension == "lproj" } - - var groupChildren: [PBXFileElement] = filePaths.map { getFileReference(path: $0, inPath: path) } - var allSourceFiles: [SourceFile] = filePaths.map { - generateSourceFile(targetType: targetType, targetSource: targetSource, path: $0, buildPhases: buildPhases) - } - var groups: [PBXGroup] = [] - - for path in directories { - - let subGroups = try getGroupSources( - targetType: targetType, - targetSource: targetSource, - path: path, - isBaseGroup: false, - hasCustomParent: false, - excludePaths: excludePaths, - includePaths: includePaths, - buildPhases: buildPhases - ) - - guard !subGroups.sourceFiles.isEmpty || project.options.generateEmptyDirectories else { - continue + return variantGroup + } + + /// Collects all the excluded paths within the targetSource + private func getSourceMatches(targetSource: TargetSource, patterns: [String]) -> Set { + let rootSourcePath = project.basePath + targetSource.path + + return Set( + patterns.parallelMap { pattern in + guard !pattern.isEmpty else { return [] } + return Glob(pattern: "\(rootSourcePath)/\(pattern)") + .map { Path($0) } + .map { + guard $0.isDirectory else { + return [$0] } - allSourceFiles += subGroups.sourceFiles + return (try? $0.recursiveChildren()) ?? [] + } + .reduce([], +) + } + .reduce([], +) + ) + } - if let firstGroup = subGroups.groups.first { - groupChildren.append(firstGroup) - groups += subGroups.groups - } else if project.options.generateEmptyDirectories { - groups += subGroups.groups - } - } + func isExcludedPattern(_ path: Path) -> Bool { + return excludePatterns.reduce(false) { + (result: Bool, expression: NSRegularExpression) -> Bool in - // find the base localised directory - let baseLocalisedDirectory: Path? = { - func findLocalisedDirectory(by languageId: String) -> Path? { - localisedDirectories.first { $0.lastComponent == "\(languageId).lproj" } - } - return findLocalisedDirectory(by: "Base") ?? - findLocalisedDirectory(by: NSLocale.canonicalLanguageIdentifier(from: project.options.developmentLanguage ?? "en")) - }() + let string: String = path.string + let range = NSRange(location: 0, length: string.count) + let matches = expression.matches(in: string, range: range) - knownRegions.formUnion(localisedDirectories.map { $0.lastComponentWithoutExtension }) - - // XCode 15 - Detect known regions from locales present in string catalogs - - let stringCatalogsLocales = stringCatalogChildren - .compactMap { StringCatalog(from: $0) } - .reduce(Set(), { partialResult, stringCatalog in - partialResult.union(stringCatalog.includedLocales) - }) - knownRegions.formUnion(stringCatalogsLocales) - - // create variant groups of the base localisation first - var baseLocalisationVariantGroups: [PBXVariantGroup] = [] - - if let baseLocalisedDirectory = baseLocalisedDirectory { - let filePaths = try baseLocalisedDirectory.children() - .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } - .sorted() - for filePath in filePaths { - let variantGroup = getVariantGroup(path: filePath, inPath: path) - groupChildren.append(variantGroup) - baseLocalisationVariantGroups.append(variantGroup) - - let sourceFile = generateSourceFile(targetType: targetType, - targetSource: targetSource, - path: filePath, - fileReference: variantGroup, - buildPhases: buildPhases) - allSourceFiles.append(sourceFile) + return result || (matches.count > 0) + } + } + + /// Checks whether the path is not in any default or TargetSource excludes + func isIncludedPath(_ path: Path, excludePaths: Set, includePaths: SortedArray?) + -> Bool + { + return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 }) + && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) + && !excludePaths.contains(path) + && !isExcludedPattern(path) + // If includes is empty, it's included. If it's not empty, the path either needs to match exactly, or it needs to be a direct parent of an included path. + && (includePaths.flatMap { _isIncludedPathSorted(path, sortedPaths: $0) } ?? true) + } + + private func _isIncludedPathSorted(_ path: Path, sortedPaths: SortedArray) -> Bool { + guard let idx = sortedPaths.firstIndex(where: { $0 >= path }) else { return false } + let foundPath = sortedPaths.value[idx] + return foundPath.description.hasPrefix(path.description) + } + + /// Gets all the children paths that aren't excluded + private func getSourceChildren( + targetSource: TargetSource, dirPath: Path, excludePaths: Set, + includePaths: SortedArray? + ) throws -> [Path] { + try dirPath.children() + .filter { + if $0.isDirectory { + let children = try $0.children() + + if children.isEmpty { + return project.options.generateEmptyDirectories + } + + return + !children + .filter { + self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } + .isEmpty + } else if $0.isFile { + return self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) + } else { + return false + } + } + } + + /// creates all the source files and groups they belong to for a given targetSource + private func getGroupSources( + targetType: PBXProductType, + targetSource: TargetSource, + path: Path, + isBaseGroup: Bool, + hasCustomParent: Bool, + excludePaths: Set, + includePaths: SortedArray?, + buildPhases: [Path: BuildPhaseSpec] + ) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) { + + let children = try getSourceChildren( + targetSource: targetSource, dirPath: path, excludePaths: excludePaths, + includePaths: includePaths) + + let createIntermediateGroups = + targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups + let nonLocalizedChildren = children.filter { $0.extension != "lproj" } + let stringCatalogChildren = children.filter { $0.extension == "xcstrings" } + + let directories = + nonLocalizedChildren + .filter { + if let fileType = getFileType(path: $0) { + return !fileType.file + } else { + return $0.isDirectory && !Xcode.isDirectoryFileWrapper(path: $0) } + } - // add references to localised resources into base localisation variant groups - for localisedDirectory in localisedDirectories { - let localisationName = localisedDirectory.lastComponentWithoutExtension - let filePaths = try localisedDirectory.children() - .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } - .sorted { $0.lastComponent < $1.lastComponent } - for filePath in filePaths { - // find base localisation variant group - // ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group - let variantGroup = baseLocalisationVariantGroups - .first { - Path($0.name!).lastComponent == filePath.lastComponent - - } ?? baseLocalisationVariantGroups.first { - Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension - } - - let fileReference = getFileReference( - path: filePath, - inPath: path, - name: variantGroup != nil ? localisationName : filePath.lastComponent - ) - - if let variantGroup = variantGroup { - if !variantGroup.children.contains(fileReference) { - variantGroup.children.append(fileReference) - } - } else { - // add SourceFile to group if there is no Base.lproj directory - let sourceFile = generateSourceFile(targetType: targetType, - targetSource: targetSource, - path: filePath, - fileReference: fileReference, - buildPhases: buildPhases) - allSourceFiles.append(sourceFile) - groupChildren.append(fileReference) - } - } + let filePaths = + nonLocalizedChildren + .filter { + if let fileType = getFileType(path: $0) { + return fileType.file + } else { + return $0.isFile || $0.isDirectory && Xcode.isDirectoryFileWrapper(path: $0) } + } - let group = getGroup( - path: path, - mergingChildren: groupChildren, - createIntermediateGroups: createIntermediateGroups, - hasCustomParent: hasCustomParent, - isBaseGroup: isBaseGroup - ) - if createIntermediateGroups { - createIntermediaGroups(for: group, at: path) - } + let localisedDirectories = + children + .filter { $0.extension == "lproj" } - groups.insert(group, at: 0) - return (allSourceFiles, groups) + var groupChildren: [PBXFileElement] = filePaths.map { getFileReference(path: $0, inPath: path) } + var allSourceFiles: [SourceFile] = filePaths.map { + generateSourceFile( + targetType: targetType, targetSource: targetSource, path: $0, buildPhases: buildPhases) } - - private func excludePatternsForPlatform(_ platform: Platform) throws - -> NSRegularExpression { - - let pattern = "\\/\(platform.rawValue)\\/" - return try NSRegularExpression(pattern: pattern) + var groups: [PBXGroup] = [] + + for path in directories { + + let subGroups = try getGroupSources( + targetType: targetType, + targetSource: targetSource, + path: path, + isBaseGroup: false, + hasCustomParent: false, + excludePaths: excludePaths, + includePaths: includePaths, + buildPhases: buildPhases + ) + + guard !subGroups.sourceFiles.isEmpty || project.options.generateEmptyDirectories else { + continue + } + + allSourceFiles += subGroups.sourceFiles + + if let firstGroup = subGroups.groups.first { + groupChildren.append(firstGroup) + groups += subGroups.groups + } else if project.options.generateEmptyDirectories { + groups += subGroups.groups + } } - /// creates source files - private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, platform: Platform? = nil, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { - - // generate excluded paths - let path = project.basePath + targetSource.path - let excludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes) - excludePatterns = targetSource.excludePatterns - if let platform = platform { - var platforms = Set(Platform.allCases) - platforms.remove(platform) - excludePatterns += try platforms.map({ - try excludePatternsForPlatform($0) - }) - } - // generate included paths. Excluded paths will override this. - let includePaths = targetSource.includes.isEmpty ? nil : getSourceMatches(targetSource: targetSource, patterns: targetSource.includes) - - let type = resolvedTargetSourceType(for: targetSource, at: path) - - let customParentGroups = (targetSource.group ?? "").split(separator: "/").map { String($0) } - let hasCustomParent = !customParentGroups.isEmpty - - let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups - - var sourceFiles: [SourceFile] = [] - let sourceReference: PBXFileElement - var sourcePath = path - switch type { - case .folder: - let fileReference = getFileReference( - path: path, - inPath: project.basePath, - name: targetSource.name ?? path.lastComponent, - sourceTree: .sourceRoot, - lastKnownFileType: "folder" - ) - - if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath { - rootGroups.insert(fileReference) - } - - let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases) - - sourceFiles.append(sourceFile) - sourceReference = fileReference - case .file: - let parentPath = path.parent() - let fileReference = getFileReference(path: path, inPath: parentPath, name: targetSource.name) - - let sourceFile = generateSourceFile(targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases) - - if hasCustomParent { - sourcePath = path - sourceReference = fileReference - } else if parentPath == project.basePath { - sourcePath = path - sourceReference = fileReference - rootGroups.insert(fileReference) - } else { - let parentGroup = getGroup( - path: parentPath, - mergingChildren: [fileReference], - createIntermediateGroups: createIntermediateGroups, - hasCustomParent: hasCustomParent, - isBaseGroup: true - ) - sourcePath = parentPath - sourceReference = parentGroup - } - sourceFiles.append(sourceFile) - - case .group: - if targetSource.optional && !path.exists { - // This group is missing, so if's optional just return an empty array - return [] - } - - let (groupSourceFiles, groups) = try getGroupSources( - targetType: targetType, - targetSource: targetSource, - path: path, - isBaseGroup: true, - hasCustomParent: hasCustomParent, - excludePaths: excludePaths, - includePaths: includePaths.flatMap(SortedArray.init(_:)), - buildPhases: buildPhases - ) + // find the base localised directory + let baseLocalisedDirectory: Path? = { + func findLocalisedDirectory(by languageId: String) -> Path? { + localisedDirectories.first { $0.lastComponent == "\(languageId).lproj" } + } + return findLocalisedDirectory(by: "Base") + ?? findLocalisedDirectory( + by: NSLocale.canonicalLanguageIdentifier( + from: project.options.developmentLanguage ?? "en")) + }() + + knownRegions.formUnion(localisedDirectories.map { $0.lastComponentWithoutExtension }) + + // XCode 15 - Detect known regions from locales present in string catalogs + + let stringCatalogsLocales = + stringCatalogChildren + .compactMap { StringCatalog(from: $0) } + .reduce( + Set(), + { partialResult, stringCatalog in + partialResult.union(stringCatalog.includedLocales) + }) + knownRegions.formUnion(stringCatalogsLocales) + + // create variant groups of the base localisation first + var baseLocalisationVariantGroups: [PBXVariantGroup] = [] + + if let baseLocalisedDirectory = baseLocalisedDirectory { + let filePaths = try baseLocalisedDirectory.children() + .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } + .sorted() + for filePath in filePaths { + let variantGroup = getVariantGroup(path: filePath, inPath: path) + groupChildren.append(variantGroup) + baseLocalisationVariantGroups.append(variantGroup) + + let sourceFile = generateSourceFile( + targetType: targetType, + targetSource: targetSource, + path: filePath, + fileReference: variantGroup, + buildPhases: buildPhases) + allSourceFiles.append(sourceFile) + } + } - let group = groups.first! - if let name = targetSource.name { - group.name = name - } + // add references to localised resources into base localisation variant groups + for localisedDirectory in localisedDirectories { + let localisationName = localisedDirectory.lastComponentWithoutExtension + let filePaths = try localisedDirectory.children() + .filter { self.isIncludedPath($0, excludePaths: excludePaths, includePaths: includePaths) } + .sorted { $0.lastComponent < $1.lastComponent } + for filePath in filePaths { + // find base localisation variant group + // ex: Foo.strings will be added to Foo.strings or Foo.storyboard variant group + let variantGroup = + baseLocalisationVariantGroups + .first { + Path($0.name!).lastComponent == filePath.lastComponent + + } + ?? baseLocalisationVariantGroups.first { + Path($0.name!).lastComponentWithoutExtension == filePath.lastComponentWithoutExtension + } + + let fileReference = getFileReference( + path: filePath, + inPath: path, + name: variantGroup != nil ? localisationName : filePath.lastComponent + ) - sourceFiles += groupSourceFiles - sourceReference = group + if let variantGroup = variantGroup { + if !variantGroup.children.contains(fileReference) { + variantGroup.children.append(fileReference) + } + } else { + // add SourceFile to group if there is no Base.lproj directory + let sourceFile = generateSourceFile( + targetType: targetType, + targetSource: targetSource, + path: filePath, + fileReference: fileReference, + buildPhases: buildPhases) + allSourceFiles.append(sourceFile) + groupChildren.append(fileReference) } + } + } - if hasCustomParent { - createParentGroups(customParentGroups, for: sourceReference) - try makePathRelative(for: sourceReference, at: path) - } else if createIntermediateGroups { - createIntermediaGroups(for: sourceReference, at: sourcePath) - } + let group = getGroup( + path: path, + mergingChildren: groupChildren, + createIntermediateGroups: createIntermediateGroups, + hasCustomParent: hasCustomParent, + isBaseGroup: isBaseGroup + ) + if createIntermediateGroups { + createIntermediaGroups(for: group, at: path) + } - return sourceFiles + groups.insert(group, at: 0) + return (allSourceFiles, groups) + } + + private func excludePatternsForPlatform(_ platform: Platform) throws + -> NSRegularExpression + { + + let pattern = "\\/\(platform.rawValue)\\/" + return try NSRegularExpression(pattern: pattern) + } + + /// Creates exclude patterns for app-group filtering (hybrid: YML > autodetect from targetName > shared) + private func excludePatternsForAppGroup(targetName: String, targetSource: TargetSource) throws + -> [NSRegularExpression] + { + // 1) Resolve appGroup (YML first) + let resolvedAppGroup: String? = { + if let fromYML = targetSource.brandName, !fromYML.isEmpty { + return fromYML + } + let lower = targetName.lowercased() + if lower.hasSuffix("_ios") { + return String(lower.dropLast(4)) + } else if lower.hasSuffix("_tvos") { + return String(lower.dropLast(5)) + } + return nil + }() + + // No appGroup → shared → no excludes + guard let brandName = resolvedAppGroup?.lowercased(), !brandName.isEmpty else { + return [] } - /// Returns the resolved `SourceType` for a given `TargetSource`. - /// - /// While `TargetSource` declares `type`, its optional and in the event that the value is not defined then we must resolve a sensible default based on the path of the source. - private func resolvedTargetSourceType(for targetSource: TargetSource, at path: Path) -> SourceType { - return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group) + // 2) Collect candidate groups + var candidates = Set() + + // 2a) from YML-defined appGroups + let ymlGroups = project.targets + .flatMap { $0.sources } + .compactMap { $0.brandName?.lowercased() } + candidates.formUnion(ymlGroups) + + // 2b) from filesystem subfolders of the current source root + let root = project.basePath + Path(targetSource.path) + if root.exists, root.isDirectory, let children = try? root.children() { + for child in children where child.isDirectory { + candidates.insert(child.lastComponent.lowercased()) + } } - private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) { - guard let parentName = parentGroups.last else { - return - } + // 3) Remove reserved/shared folders + let reserved: Set = ["common", "ios", "tvos"] + candidates.subtract(reserved) - let parentPath = project.basePath + Path(parentGroups.joined(separator: "/")) - let parentPathExists = parentPath.exists - let parentGroupAlreadyExists = groupsByPath[parentPath] != nil + // 4) Build case-insensitive regexes for all "other" groups + var patterns: [NSRegularExpression] = [] + for other in candidates where other != brandName { + let pattern = "\\/\(NSRegularExpression.escapedPattern(for: other))\\/" + patterns.append(try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])) + } + return patterns + } + + /// Creates exclude patterns for brandName filtering + // private func excludePatternsForBrand(targetName: String, targetSource: TargetSource) throws + // -> [NSRegularExpression] { + // let resolvedBrandName: String? = { + // if let fromYML = targetSource.brandName { + // return fromYML + // } + // let lowerName = targetName.lowercased() + // if lowerName.hasSuffix("_ios") { + // return String(lowerName.dropLast(4)) + // } else if lowerName.hasSuffix("_tvos") { + // return String(lowerName.dropLast(5)) + // } + // return nil + // }() + // + // guard let brandName = resolvedBrandName else { + // return [] + // } + // + // let allGroups = project.targets + // .flatMap { $0.sources } + // .compactMap { $0.brandName } + // .reduce(into: Set()) { $0.insert($1) } + // + // var patterns: [NSRegularExpression] = [] + // for other in allGroups where other != brandName { + // let pattern = "\\/\(other)\\/" + // patterns.append(try NSRegularExpression(pattern: pattern)) + // } + // + // return patterns + // } + + /// creates source files + private func getSourceFiles( + targetName: String, targetType: PBXProductType, targetSource: TargetSource, + platform: Platform? = nil, buildPhases: [Path: BuildPhaseSpec] + ) throws -> [SourceFile] { + + // generate excluded paths + let path = project.basePath + targetSource.path + let excludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes) + excludePatterns = targetSource.excludePatterns + if let platform = platform { + var platforms = Set(Platform.allCases) + platforms.remove(platform) + excludePatterns += try platforms.map({ + try excludePatternsForPlatform($0) + }) + } + excludePatterns += try excludePatternsForAppGroup( + targetName: targetName, targetSource: targetSource) + + // generate included paths. Excluded paths will override this. + let includePaths = + targetSource.includes.isEmpty + ? nil : getSourceMatches(targetSource: targetSource, patterns: targetSource.includes) + + let type = resolvedTargetSourceType(for: targetSource, at: path) + + let customParentGroups = (targetSource.group ?? "").split(separator: "/").map { String($0) } + let hasCustomParent = !customParentGroups.isEmpty + + let createIntermediateGroups = + targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups + + var sourceFiles: [SourceFile] = [] + let sourceReference: PBXFileElement + var sourcePath = path + switch type { + case .folder: + let fileReference = getFileReference( + path: path, + inPath: project.basePath, + name: targetSource.name ?? path.lastComponent, + sourceTree: .sourceRoot, + lastKnownFileType: "folder" + ) + + if !(createIntermediateGroups || hasCustomParent) || path.parent() == project.basePath { + rootGroups.insert(fileReference) + } + + let sourceFile = generateSourceFile( + targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases) + + sourceFiles.append(sourceFile) + sourceReference = fileReference + case .file: + let parentPath = path.parent() + let fileReference = getFileReference(path: path, inPath: parentPath, name: targetSource.name) + + let sourceFile = generateSourceFile( + targetType: targetType, targetSource: targetSource, path: path, buildPhases: buildPhases) + + if hasCustomParent { + sourcePath = path + sourceReference = fileReference + } else if parentPath == project.basePath { + sourcePath = path + sourceReference = fileReference + rootGroups.insert(fileReference) + } else { let parentGroup = getGroup( - path: parentPath, - mergingChildren: [fileElement], - createIntermediateGroups: false, - hasCustomParent: false, - isBaseGroup: parentGroups.count == 1 + path: parentPath, + mergingChildren: [fileReference], + createIntermediateGroups: createIntermediateGroups, + hasCustomParent: hasCustomParent, + isBaseGroup: true ) - - // As this path is a custom group, remove the path reference - if !parentPathExists { - parentGroup.name = String(parentName) - parentGroup.path = nil - } - - if !parentGroupAlreadyExists { - createParentGroups(parentGroups.dropLast(), for: parentGroup) - } + sourcePath = parentPath + sourceReference = parentGroup + } + sourceFiles.append(sourceFile) + + case .group: + if targetSource.optional && !path.exists { + // This group is missing, so if's optional just return an empty array + return [] + } + + let (groupSourceFiles, groups) = try getGroupSources( + targetType: targetType, + targetSource: targetSource, + path: path, + isBaseGroup: true, + hasCustomParent: hasCustomParent, + excludePaths: excludePaths, + includePaths: includePaths.flatMap(SortedArray.init(_:)), + buildPhases: buildPhases + ) + + let group = groups.first! + if let name = targetSource.name { + group.name = name + } + + sourceFiles += groupSourceFiles + sourceReference = group } - // Add groups for all parents recursively - private func createIntermediaGroups(for fileElement: PBXFileElement, at path: Path) { + if hasCustomParent { + createParentGroups(customParentGroups, for: sourceReference) + try makePathRelative(for: sourceReference, at: path) + } else if createIntermediateGroups { + createIntermediaGroups(for: sourceReference, at: sourcePath) + } - let parentPath = path.parent() - guard parentPath != project.basePath else { - // we've reached the top - return - } + return sourceFiles + } - let hasParentGroup = groupsByPath[parentPath] != nil - if !hasParentGroup { - do { - // if the path is a parent of the project base path (or if calculating that fails) - // do not create a parent group - // e.g. for project path foo/bar/baz - // - create foo/baz - // - create baz/ - // - do not create foo - let pathIsParentOfProject = try path.isParent(of: project.basePath) - if pathIsParentOfProject { return } - } catch { - return - } - } - let parentGroup = getGroup( - path: parentPath, - mergingChildren: [fileElement], - createIntermediateGroups: true, - hasCustomParent: false, - isBaseGroup: false - ) + /// Returns the resolved `SourceType` for a given `TargetSource`. + /// + /// While `TargetSource` declares `type`, its optional and in the event that the value is not defined then we must resolve a sensible default based on the path of the source. + private func resolvedTargetSourceType(for targetSource: TargetSource, at path: Path) -> SourceType + { + return targetSource.type ?? (path.isFile || path.extension != nil ? .file : .group) + } - if !hasParentGroup { - createIntermediaGroups(for: parentGroup, at: parentPath) - } + private func createParentGroups(_ parentGroups: [String], for fileElement: PBXFileElement) { + guard let parentName = parentGroups.last else { + return } - // Make the fileElement path and name relative to its parents aggregated paths - private func makePathRelative(for fileElement: PBXFileElement, at path: Path) throws { - // This makes the fileElement path relative to its parent and not to the project. Xcode then rebuilds the actual - // path for the file based on the hierarchy this fileElement lives in. - var paths: [String] = [] - var element: PBXFileElement = fileElement - while true { - guard let parent = element.parent else { break } + let parentPath = project.basePath + Path(parentGroups.joined(separator: "/")) + let parentPathExists = parentPath.exists + let parentGroupAlreadyExists = groupsByPath[parentPath] != nil + + let parentGroup = getGroup( + path: parentPath, + mergingChildren: [fileElement], + createIntermediateGroups: false, + hasCustomParent: false, + isBaseGroup: parentGroups.count == 1 + ) + + // As this path is a custom group, remove the path reference + if !parentPathExists { + parentGroup.name = String(parentName) + parentGroup.path = nil + } - if let path = parent.path { - paths.insert(path, at: 0) - } + if !parentGroupAlreadyExists { + createParentGroups(parentGroups.dropLast(), for: parentGroup) + } + } - element = parent - } + // Add groups for all parents recursively + private func createIntermediaGroups(for fileElement: PBXFileElement, at path: Path) { - let completePath = project.basePath + Path(paths.joined(separator: "/")) - let relativePath = try path.relativePath(from: completePath) - let relativePathString = relativePath.string + let parentPath = path.parent() + guard parentPath != project.basePath else { + // we've reached the top + return + } - if relativePathString != fileElement.path { - fileElement.path = relativePathString - fileElement.name = relativePath.lastComponent - } + let hasParentGroup = groupsByPath[parentPath] != nil + if !hasParentGroup { + do { + // if the path is a parent of the project base path (or if calculating that fails) + // do not create a parent group + // e.g. for project path foo/bar/baz + // - create foo/baz + // - create baz/ + // - do not create foo + let pathIsParentOfProject = try path.isParent(of: project.basePath) + if pathIsParentOfProject { return } + } catch { + return + } + } + let parentGroup = getGroup( + path: parentPath, + mergingChildren: [fileElement], + createIntermediateGroups: true, + hasCustomParent: false, + isBaseGroup: false + ) + + if !hasParentGroup { + createIntermediaGroups(for: parentGroup, at: parentPath) + } + } + + // Make the fileElement path and name relative to its parents aggregated paths + private func makePathRelative(for fileElement: PBXFileElement, at path: Path) throws { + // This makes the fileElement path relative to its parent and not to the project. Xcode then rebuilds the actual + // path for the file based on the hierarchy this fileElement lives in. + var paths: [String] = [] + var element: PBXFileElement = fileElement + while true { + guard let parent = element.parent else { break } + + if let path = parent.path { + paths.insert(path, at: 0) + } + + element = parent } - private func findCurrentCoreDataModelVersionPath(using versionedModels: [Path]) -> Path? { - // Find and parse the current version model stored in the .xccurrentversion file - guard - let versionPath = versionedModels.first(where: { $0.lastComponent == ".xccurrentversion" }), - let data = try? versionPath.read(), - let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any], - let versionString = plist["_XCCurrentVersionName"] as? String else { - return nil - } - return versionedModels.first(where: { $0.lastComponent == versionString }) + let completePath = project.basePath + Path(paths.joined(separator: "/")) + let relativePath = try path.relativePath(from: completePath) + let relativePathString = relativePath.string + + if relativePathString != fileElement.path { + fileElement.path = relativePathString + fileElement.name = relativePath.lastComponent + } + } + + private func findCurrentCoreDataModelVersionPath(using versionedModels: [Path]) -> Path? { + // Find and parse the current version model stored in the .xccurrentversion file + guard + let versionPath = versionedModels.first(where: { $0.lastComponent == ".xccurrentversion" }), + let data = try? versionPath.read(), + let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) + as? [String: Any], + let versionString = plist["_XCCurrentVersionName"] as? String + else { + return nil } + return versionedModels.first(where: { $0.lastComponent == versionString }) + } }