Files
XcodeGen/Sources/XcodeGenKit/PBXProjGenerator.swift
T
Brandon Kase 4a54fe6d2a Support file sources
The `sources` key of the project spec only supported directories and not
files. Now it supports both!

This commit introduces a `getSourceFiles` overload that doesn't explicitly
invoke `path.children()`, but instead accepts `children` as a parameter.
This allows us to invoke the `children` overload of getSourceFiles with just
the files we want to include (determined by specifying the sources).

Now for sourcePaths that are files, we group by parents before invoking
getSourceFiles in order to reuse the same groups.
2017-11-01 11:37:19 -07:00

625 lines
30 KiB
Swift

//
// PBXProjGenerator.swift
// XcodeGen
//
// Created by Yonas Kolb on 23/7/17.
//
//
import Foundation
import Foundation
import PathKit
import xcproj
import JSONUtilities
import Yams
import ProjectSpec
public class PBXProjGenerator {
let spec: ProjectSpec
let currentXcodeVersion: String
var fileReferencesByPath: [Path: String] = [:]
var groupsByPath: [Path: PBXGroup] = [:]
var variantGroupsByPath: [Path: PBXVariantGroup] = [:]
var targetNativeReferences: [String: String] = [:]
var targetBuildFiles: [String: PBXBuildFile] = [:]
var targetFileReferences: [String: String] = [:]
var topLevelGroups: [PBXGroup] = []
var carthageFrameworksByPlatform: [String: Set<String>] = [:]
var frameworkFiles: [String] = []
var uuids: Set<String> = []
var project: PBXProj!
var carthageBuildPath: String {
return spec.options.carthageBuildPath ?? "Carthage/Build"
}
public init(spec: ProjectSpec, currentXcodeVersion: String) {
self.currentXcodeVersion = currentXcodeVersion
self.spec = spec
}
public func generateUUID<T: PBXObject>(_ element: T.Type, _ id: String) -> String {
var uuid: String = ""
var counter: UInt = 0
let className: String = String(describing: T.self).replacingOccurrences(of: "PBX", with: "")
let classAcronym = String(className.characters.filter { String($0).lowercased() != String($0) })
let stringID = String(abs(id.hashValue).description.characters.prefix(10 - classAcronym.characters.count))
repeat {
counter += 1
uuid = "\(classAcronym)\(stringID)\(String(format: "%02d", counter))"
} while (uuids.contains(uuid))
uuids.insert(uuid)
return uuid
}
func addObject(_ object: PBXObject) {
project.addObject(object)
}
public func generate() throws -> PBXProj {
uuids = []
project = PBXProj(objectVersion: 46, rootObject: generateUUID(PBXProject.self, spec.name))
for group in spec.fileGroups {
//TODO: call a seperate function that only creates groups not source files
let path = spec.basePath + group
_ = try getSourceFiles(path: path, children: try path.children())
}
let buildConfigs: [XCBuildConfiguration] = spec.configs.map { config in
let buildSettings = spec.getProjectBuildSettings(config: config)
var baseConfigurationReference: String?
if let configPath = spec.configFiles[config.name] {
baseConfigurationReference = getFileReference(path: spec.basePath + configPath, inPath: spec.basePath)
}
return XCBuildConfiguration(reference: generateUUID(XCBuildConfiguration.self, config.name), name: config.name, baseConfigurationReference: baseConfigurationReference, buildSettings: buildSettings)
}
let buildConfigList = XCConfigurationList(reference: generateUUID(XCConfigurationList.self, spec.name), buildConfigurations: buildConfigs.references, defaultConfigurationName: buildConfigs.first?.name ?? "", defaultConfigurationIsVisible: 0)
buildConfigs.forEach(addObject)
addObject(buildConfigList)
for target in spec.targets {
targetNativeReferences[target.name] = generateUUID(PBXNativeTarget.self, target.name)
let fileReference = PBXFileReference(reference: generateUUID(PBXFileReference.self, target.name), sourceTree: .buildProductsDir, explicitFileType: target.type.fileExtension, path: target.filename, includeInIndex: 0)
addObject(fileReference)
targetFileReferences[target.name] = fileReference.reference
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference.reference), fileRef: fileReference.reference)
addObject(buildFile)
targetBuildFiles[target.name] = buildFile
}
let targets = try spec.targets.map(generateTarget)
let productGroup = PBXGroup(reference: generateUUID(PBXGroup.self, "Products"), children: Array(targetFileReferences.values), sourceTree: .group, name: "Products")
addObject(productGroup)
topLevelGroups.append(productGroup)
if !carthageFrameworksByPlatform.isEmpty {
var platforms: [PBXGroup] = []
for (platform, fileReferences) in carthageFrameworksByPlatform {
let platformGroup = PBXGroup(reference: generateUUID(PBXGroup.self, platform), children: fileReferences.sorted(), sourceTree: .group, name: platform, path: platform)
addObject(platformGroup)
platforms.append(platformGroup)
}
let carthageGroup = PBXGroup(reference: generateUUID(PBXGroup.self, "Carthage"), children: platforms.references.sorted(), sourceTree: .group, name: "Carthage", path: carthageBuildPath)
addObject(carthageGroup)
frameworkFiles.append(carthageGroup.reference)
}
if !frameworkFiles.isEmpty {
let group = PBXGroup(reference: generateUUID(PBXGroup.self, "Frameworks"), children: frameworkFiles, sourceTree: .group, name: "Frameworks")
addObject(group)
topLevelGroups.append(group)
}
let mainGroup = PBXGroup(reference: generateUUID(PBXGroup.self, "Project"), children: topLevelGroups.references, sourceTree: .group)
addObject(mainGroup)
let knownRegions: [String] = ["en", "Base"]
let projectAttributes: [String: Any] = ["LastUpgradeCheck": currentXcodeVersion].merged(spec.attributes)
let root = PBXProject(name: spec.name,
reference: project.rootObject,
buildConfigurationList: buildConfigList.reference,
compatibilityVersion: "Xcode 3.2",
mainGroup: mainGroup.reference,
developmentRegion: "English",
knownRegions: knownRegions,
targets: targets.references,
attributes: projectAttributes)
project.projects.append(root)
return project
}
struct SourceFile {
let path: Path
let fileReference: String
let buildFile: PBXBuildFile
}
func generateSourceFile(path: Path) -> SourceFile {
let fileReference = fileReferencesByPath[path]!
var settings: [String: Any]?
if getBuildPhaseForPath(path) == .headers {
settings = ["ATTRIBUTES": ["Public"]]
}
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference), fileRef: fileReference, settings: settings)
return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile)
}
func generateTarget(_ target: Target) throws -> PBXNativeTarget {
let carthageDependencies = getAllCarthageDependencies(target: target)
let sourceFiles = try getAllSourceFiles(sources: target.sources)
// find all Info.plist files
let infoPlists: [Path] = target.sources.map { spec.basePath + $0.path }.flatMap { (path) -> [Path] in
if path.isFile {
if path.lastComponent == "Info.plist" {
return [path]
}
} else {
if let children = try? path.recursiveChildren() {
return children.filter { $0.lastComponent == "Info.plist" }
}
}
return []
}
let configs: [XCBuildConfiguration] = spec.configs.map { config in
var buildSettings = spec.getTargetBuildSettings(target: target, config: config)
// automatically set INFOPLIST_FILE path
if let plistPath = infoPlists.first,
!spec.targetHasBuildSetting("INFOPLIST_FILE", basePath: spec.basePath, target: target, config: config) {
buildSettings["INFOPLIST_FILE"] = plistPath.byRemovingBase(path: spec.basePath)
}
// automatically calculate bundle id
if let bundleIdPrefix = spec.options.bundleIdPrefix,
!spec.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", basePath: spec.basePath, 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,
!spec.targetHasBuildSetting("TEST_TARGET_NAME", basePath: spec.basePath, target: target, config: config) {
for dependency in target.dependencies {
if dependency.type == .target,
let dependencyTarget = spec.getTarget(dependency.reference),
dependencyTarget.type == .application {
buildSettings["TEST_TARGET_NAME"] = dependencyTarget.name
break
}
}
}
// set Carthage search paths
if !carthageDependencies.isEmpty {
let frameworkSearchPaths = "FRAMEWORK_SEARCH_PATHS"
let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + getCarthageBuildPath(platform: target.platform)
var newSettings: [String] = []
if var array = buildSettings[frameworkSearchPaths] as? [String] {
array.append(carthagePlatformBuildPath)
buildSettings[frameworkSearchPaths] = array
} else if let string = buildSettings[frameworkSearchPaths] as? String {
buildSettings[frameworkSearchPaths] = [string, carthagePlatformBuildPath]
} else {
buildSettings[frameworkSearchPaths] = ["$(inherited)", carthagePlatformBuildPath]
}
}
var baseConfigurationReference: String?
if let configPath = target.configFiles[config.name] {
baseConfigurationReference = getFileReference(path: spec.basePath + configPath, inPath: spec.basePath)
}
return XCBuildConfiguration(reference: generateUUID(XCBuildConfiguration.self, config.name + target.name), name: config.name, baseConfigurationReference: baseConfigurationReference, buildSettings: buildSettings)
}
configs.forEach(addObject)
let buildConfigList = XCConfigurationList(reference: generateUUID(XCConfigurationList.self, target.name), buildConfigurations: configs.references, defaultConfigurationName: "")
addObject(buildConfigList)
var dependencies: [String] = []
var targetFrameworkBuildFiles: [String] = []
var copyFrameworksReferences: [String] = []
var copyResourcesReferences: [String] = []
var copyWatchReferences: [String] = []
var extensions: [String] = []
for dependency in target.dependencies {
let embed = dependency.embed ?? (target.type.isApp ? true : false)
switch dependency.type {
case .target:
let dependencyTargetName = dependency.reference
guard let dependencyTarget = spec.getTarget(dependencyTargetName) else { continue }
let dependencyFileReference = targetFileReferences[dependencyTargetName]!
let targetProxy = PBXContainerItemProxy(reference: generateUUID(PBXContainerItemProxy.self, target.name), containerPortal: project.rootObject, remoteGlobalIDString: targetNativeReferences[dependencyTargetName]!, proxyType: .nativeTarget, remoteInfo: dependencyTargetName)
let targetDependency = PBXTargetDependency(reference: generateUUID(PBXTargetDependency.self, dependencyTargetName + target.name), target: targetNativeReferences[dependencyTargetName]!, targetProxy: targetProxy.reference)
addObject(targetProxy)
addObject(targetDependency)
dependencies.append(targetDependency.reference)
if (dependencyTarget.type.isLibrary || dependencyTarget.type.isFramework) && dependency.link {
let dependencyBuildFile = targetBuildFiles[dependencyTargetName]!
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, dependencyBuildFile.reference + target.name), fileRef: dependencyBuildFile.fileRef!)
addObject(buildFile)
targetFrameworkBuildFiles.append(buildFile.reference)
}
if embed && !dependencyTarget.type.isLibrary {
let embedSettings = dependency.buildSettings
let embedFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, dependencyFileReference + target.name), fileRef: dependencyFileReference, settings: embedSettings)
addObject(embedFile)
if dependencyTarget.type.isExtension {
// embed app extension
extensions.append(embedFile.reference)
} else if dependencyTarget.type.isFramework {
copyFrameworksReferences.append(embedFile.reference)
} else if dependencyTarget.type.isApp && dependencyTarget.platform == .watchOS {
copyWatchReferences.append(embedFile.reference)
} else {
copyResourcesReferences.append(embedFile.reference)
}
}
case .framework:
let fileReference = getFileReference(path: Path(dependency.reference), inPath: spec.basePath)
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference)
addObject(buildFile)
targetFrameworkBuildFiles.append(buildFile.reference)
if !frameworkFiles.contains(fileReference) {
frameworkFiles.append(fileReference)
}
if embed {
let embedFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference, settings: dependency.buildSettings)
addObject(embedFile)
copyFrameworksReferences.append(embedFile.reference)
}
case .carthage:
var platformPath = Path(getCarthageBuildPath(platform: target.platform))
var frameworkPath = platformPath + dependency.reference
if frameworkPath.extension == nil {
frameworkPath = Path(frameworkPath.string + ".framework")
}
let fileReference = getFileReference(path: frameworkPath, inPath: platformPath)
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference)
addObject(buildFile)
carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference)
targetFrameworkBuildFiles.append(buildFile.reference)
if target.platform == .macOS && target.type.isApp {
let embedFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference + target.name), fileRef: fileReference, settings: dependency.buildSettings)
addObject(embedFile)
copyFrameworksReferences.append(embedFile.reference)
}
}
}
let fileReference = targetFileReferences[target.name]!
var buildPhases: [String] = []
func getBuildFilesForPhase(_ buildPhase: BuildPhase) -> [String] {
let files = sourceFiles.filter { getBuildPhaseForPath($0.path) == buildPhase }
files.forEach { addObject($0.buildFile) }
return files.map { $0.buildFile.reference }
}
func getBuildScript(buildScript: BuildScript) throws -> PBXShellScriptBuildPhase {
var shellScript: String
switch buildScript.script {
case let .path(path):
shellScript = try (spec.basePath + path).read()
case let .script(script):
shellScript = script
}
shellScript = shellScript.replacingOccurrences(of: "\"", with: "\\\"") // TODO: remove when xcodeproj escaped values
let shellScriptPhase = PBXShellScriptBuildPhase(
reference: generateUUID(PBXShellScriptBuildPhase.self, String(describing: buildScript.name) + shellScript + target.name),
files: [],
name: buildScript.name ?? "Run Script",
inputPaths: buildScript.inputFiles,
outputPaths: buildScript.outputFiles,
shellPath: buildScript.shell ?? "/bin/sh",
shellScript: shellScript)
shellScriptPhase.runOnlyForDeploymentPostprocessing = buildScript.runOnlyWhenInstalling ? 1 : 0
addObject(shellScriptPhase)
buildPhases.append(shellScriptPhase.reference)
return shellScriptPhase
}
_ = try target.prebuildScripts.map(getBuildScript)
let sourcesBuildPhase = PBXSourcesBuildPhase(reference: generateUUID(PBXSourcesBuildPhase.self, target.name), files: getBuildFilesForPhase(.sources))
addObject(sourcesBuildPhase)
buildPhases.append(sourcesBuildPhase.reference)
let resourcesBuildPhase = PBXResourcesBuildPhase(reference: generateUUID(PBXResourcesBuildPhase.self, target.name), files: getBuildFilesForPhase(.resources) + copyResourcesReferences)
addObject(resourcesBuildPhase)
buildPhases.append(resourcesBuildPhase.reference)
if target.type == .framework || target.type == .dynamicLibrary {
let headersBuildPhase = PBXHeadersBuildPhase(reference: generateUUID(PBXHeadersBuildPhase.self, target.name), files: getBuildFilesForPhase(.headers))
addObject(headersBuildPhase)
buildPhases.append(headersBuildPhase.reference)
}
if !targetFrameworkBuildFiles.isEmpty {
let frameworkBuildPhase = PBXFrameworksBuildPhase(
reference: generateUUID(PBXFrameworksBuildPhase.self, target.name),
files: targetFrameworkBuildFiles,
runOnlyForDeploymentPostprocessing: 0)
addObject(frameworkBuildPhase)
buildPhases.append(frameworkBuildPhase.reference)
}
if !extensions.isEmpty {
let copyFilesPhase = PBXCopyFilesBuildPhase(
reference: generateUUID(PBXCopyFilesBuildPhase.self, "embed app extensions" + target.name),
dstPath: "",
dstSubfolderSpec: .plugins,
files: extensions)
addObject(copyFilesPhase)
buildPhases.append(copyFilesPhase.reference)
}
if !copyFrameworksReferences.isEmpty {
let copyFilesPhase = PBXCopyFilesBuildPhase(
reference: generateUUID(PBXCopyFilesBuildPhase.self, "embed frameworks" + target.name),
dstPath: "",
dstSubfolderSpec: .frameworks,
files: copyFrameworksReferences)
addObject(copyFilesPhase)
buildPhases.append(copyFilesPhase.reference)
}
if !copyWatchReferences.isEmpty {
let copyFilesPhase = PBXCopyFilesBuildPhase(
reference: generateUUID(PBXCopyFilesBuildPhase.self, "embed watch content" + target.name),
dstPath: "$(CONTENTS_FOLDER_PATH)/Watch",
dstSubfolderSpec: .productsDirectory,
files: copyWatchReferences)
addObject(copyFilesPhase)
buildPhases.append(copyFilesPhase.reference)
}
let carthageFrameworksToEmbed = Array(Set(carthageDependencies
.filter { $0.embed ?? true }
.map { $0.reference }))
.sorted()
if !carthageFrameworksToEmbed.isEmpty {
if target.type.isApp && target.platform != .macOS {
let inputPaths = carthageFrameworksToEmbed.map { "$(SRCROOT)/\(carthageBuildPath)/\(target.platform)/\($0)\($0.contains(".") ? "" : ".framework")" }
let outputPaths = carthageFrameworksToEmbed.map { "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/\($0)\($0.contains(".") ? "" : ".framework")" }
let carthageScript = PBXShellScriptBuildPhase(reference: generateUUID(PBXShellScriptBuildPhase.self, "Carthage" + target.name), files: [], name: "Carthage", inputPaths: inputPaths, outputPaths: outputPaths, shellPath: "/bin/sh", shellScript: "/usr/local/bin/carthage copy-frameworks\n")
addObject(carthageScript)
buildPhases.append(carthageScript.reference)
}
}
_ = try target.postbuildScripts.map(getBuildScript)
let nativeTarget = PBXNativeTarget(
reference: targetNativeReferences[target.name]!,
name: target.name,
buildConfigurationList: buildConfigList.reference,
buildPhases: buildPhases,
buildRules: [],
dependencies: dependencies,
productReference: fileReference,
productType: target.type)
addObject(nativeTarget)
return nativeTarget
}
func getCarthageBuildPath(platform: Platform) -> String {
let carthagePath = Path(carthageBuildPath)
let platformName = platform.carthageDirectoryName
return "\(carthagePath)/\(platformName)"
}
func getAllCarthageDependencies(target: Target) -> [Dependency] {
var frameworks: [Dependency] = []
for dependency in target.dependencies {
switch dependency.type {
case .carthage:
frameworks.append(dependency)
case .target:
if let target = spec.getTarget(dependency.reference) {
frameworks += getAllCarthageDependencies(target: target)
}
default: break
}
}
return frameworks
}
func getBuildPhaseForPath(_ path: Path) -> BuildPhase? {
if path.lastComponent == "Info.plist" {
return nil
}
if let fileExtension = path.extension {
switch fileExtension {
case "swift", "m", "cpp": return .sources
case "h", "hh", "hpp", "ipp", "tpp", "hxx", "def": return .headers
case "xcconfig", "entitlements", "gpx", "lproj", "apns": return nil
default: return .resources
}
}
return nil
}
func getFileReference(path: Path, inPath: Path) -> String {
if let fileReference = fileReferencesByPath[path] {
return fileReference
} else {
let fileReference = PBXFileReference(reference: generateUUID(PBXFileReference.self, path.lastComponent), sourceTree: .group, path: path.byRemovingBase(path: inPath).string)
addObject(fileReference)
fileReferencesByPath[path] = fileReference.reference
return fileReference.reference
}
}
func getAllSourceFiles(sources: [Source]) throws -> [SourceFile] {
let sourcePaths = sources.map{ spec.basePath + $0.path }
let (files, dirs) = (sourcePaths.filter{ $0.isFile }, sourcePaths.filter{ $0.isDirectory })
let filesByParent: [Path: [Path]] = files.reduce([:]) { acc, file in
var mut = acc
let group = file.parent()
mut[group, default: []].append(file)
return mut
}
let fromFiles = try filesByParent.map{ parent, files in
try getSourceFiles(path: parent, children: files)
}
let fromDirs = try dirs.map{ dir in
try getSourceFiles(path: dir, children: try dir.children())
}
return (fromFiles + fromDirs).flatMap{ $0.sourceFiles }
}
func getSourceFiles(path: Path, children: [Path], depth: Int = 0) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) {
let excludedFiles: [String] = [".DS_Store"]
let directories = children
.filter { $0.isDirectory && $0.extension == nil && $0.extension != "lproj" }
.sorted { $0.lastComponent < $1.lastComponent }
let filePaths = children
.filter { $0.isFile || $0.extension != nil && $0.extension != "lproj" }
.filter { !excludedFiles.contains($0.lastComponent) }
.sorted { $0.lastComponent < $1.lastComponent }
let localisedDirectories = children
.filter { $0.extension == "lproj" }
.sorted { $0.lastComponent < $1.lastComponent }
var groupChildren: [String] = filePaths.map { getFileReference(path: $0, inPath: path) }
var allSourceFiles: [SourceFile] = filePaths.map { generateSourceFile(path: $0) }
var groups: [PBXGroup] = []
for path in directories {
let subGroups = try getSourceFiles(path: path, children: try path.children(), depth: depth + 1)
allSourceFiles += subGroups.sourceFiles
groupChildren.append(subGroups.groups.first!.reference)
groups += subGroups.groups
}
// create variant groups of the base localisation first
var baseLocalisationVariantGroups: [PBXVariantGroup] = []
if let baseLocalisedDirectory = localisedDirectories.first(where: { $0.lastComponent == "Base.lproj" }) {
for path in try baseLocalisedDirectory.children() {
let filePath = "\(baseLocalisedDirectory.lastComponent)/\(path.lastComponent)"
let variantGroup: PBXVariantGroup
if let cachedGroup = variantGroupsByPath[path] {
variantGroup = cachedGroup
} else {
variantGroup = PBXVariantGroup(reference: generateUUID(PBXVariantGroup.self, filePath),
children: [],
name: path.lastComponent,
sourceTree: .group)
variantGroupsByPath[path] = variantGroup
addObject(variantGroup)
groupChildren.append(variantGroup.reference)
}
baseLocalisationVariantGroups.append(variantGroup)
let buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, variantGroup.reference), fileRef: variantGroup.reference, settings: nil)
allSourceFiles.append(SourceFile(path: path, fileReference: variantGroup.reference, buildFile: buildFile))
}
}
// add references to localised resources into base localisation variant groups
for localisedDirectory in localisedDirectories {
let localisationName = localisedDirectory.lastComponentWithoutExtension
for path in try localisedDirectory.children().sorted { $0.lastComponent < $1.lastComponent } {
let filePath = "\(localisedDirectory.lastComponent)/\(path.lastComponent)"
// find base localisation variant group
let name = path.lastComponentWithoutExtension
let variantGroup = baseLocalisationVariantGroups.first { Path($0.name!).lastComponentWithoutExtension == name }
let fileReference: String
if let cachedFileReference = fileReferencesByPath[path] {
fileReference = cachedFileReference
} else {
let reference = PBXFileReference(reference: generateUUID(PBXFileReference.self, path.lastComponent),
sourceTree: .group,
name: variantGroup != nil ? localisationName : path.lastComponent,
path: filePath)
addObject(reference)
fileReference = reference.reference
fileReferencesByPath[path] = fileReference
}
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 buildFile = PBXBuildFile(reference: generateUUID(PBXBuildFile.self, fileReference),
fileRef: fileReference,
settings: nil)
allSourceFiles.append(SourceFile(path: path, fileReference: fileReference, buildFile: buildFile))
groupChildren.append(fileReference)
}
}
}
let groupPath: String = depth == 0 ? path.byRemovingBase(path: spec.basePath).string : path.lastComponent
let group: PBXGroup
if let cachedGroup = groupsByPath[path] {
group = cachedGroup
} else {
group = PBXGroup(reference: generateUUID(PBXGroup.self, path.lastComponent), children: groupChildren, sourceTree: .group, name: path.lastComponent, path: groupPath)
addObject(group)
if depth == 0 {
topLevelGroups.append(group)
}
groupsByPath[path] = group
}
groups.insert(group, at: 0)
return (allSourceFiles, groups)
}
}