mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
e48045da9e
This commit adds a new option `createIntermediateGroups` that defaults to false. When it is false, the behavior of XcodeGen is the same as before. When it is true, we make intermediate groups recursively until we reach the basePath. In practice that means if you've chosen `Platform/PINFoundation/Sources` as one of your sourcePaths, you get a top-level group of `Platform` and under that `PINFoundation` and under that `Sources`. This is instead of the default behavior of just making `Sources` a top-level group (which is confusing when your directory is called `Sources` for example).
666 lines
31 KiB
Swift
666 lines
31 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
|
|
_ = try getSources(path: spec.basePath + group)
|
|
}
|
|
|
|
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", "mm", "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 getSources(path: parent, children: files)
|
|
}
|
|
let fromDirs = try dirs.map { dir in
|
|
try getSources(path: dir)
|
|
}
|
|
|
|
return (fromFiles + fromDirs).flatMap { $0.sourceFiles }
|
|
}
|
|
|
|
func getSingleGroup(path: Path, mergingChildren children: [String], depth: Int = 0) -> PBXGroup {
|
|
let group: PBXGroup
|
|
if let cachedGroup = groupsByPath[path] {
|
|
cachedGroup.children += children
|
|
group = cachedGroup
|
|
} else {
|
|
group = PBXGroup(
|
|
reference: generateUUID(PBXGroup.self, path.lastComponent),
|
|
children: children,
|
|
sourceTree: .group,
|
|
name: path.lastComponent,
|
|
path: depth == 0 && !spec.options.createIntermediateGroups ?
|
|
path.byRemovingBase(path: spec.basePath).string :
|
|
path.lastComponent
|
|
)
|
|
addObject(group)
|
|
groupsByPath[path] = group
|
|
}
|
|
return group
|
|
}
|
|
|
|
// Add groups for all parents recursively
|
|
// ex: path/foo/bar/baz/Hello.swift -> path:[foo:[bar:[baz:[Hello.swift]]]]
|
|
func getIntermediateGroups(path: Path, group: PBXGroup) -> PBXGroup {
|
|
// verify path is a subpath of spec.basePath
|
|
guard Path(components: zip(path.components, spec.basePath.components).map{ $0.0 }) == spec.basePath else {
|
|
return group
|
|
}
|
|
|
|
// base case
|
|
if path == spec.basePath {
|
|
return group
|
|
}
|
|
|
|
// recursive case
|
|
return getIntermediateGroups(
|
|
path: path.parent(),
|
|
group: getSingleGroup(path: path, mergingChildren: [group.reference])
|
|
)
|
|
}
|
|
|
|
func getSources(path: Path, children: [Path]? = nil, depth: Int = 0) throws -> (sourceFiles: [SourceFile], groups: [PBXGroup]) {
|
|
let children = try children ?? (try path.children())
|
|
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 getSources(path: path, 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 group: PBXGroup
|
|
if spec.options.createIntermediateGroups {
|
|
group = getIntermediateGroups(
|
|
path: path.parent(),
|
|
group: getSingleGroup(path: path, mergingChildren: groupChildren, depth: depth)
|
|
)
|
|
} else {
|
|
group = getSingleGroup(path: path, mergingChildren: groupChildren, depth: depth)
|
|
}
|
|
|
|
if depth == 0 {
|
|
topLevelGroups.append(group)
|
|
}
|
|
groups.insert(group, at: 0)
|
|
return (allSourceFiles, groups)
|
|
}
|
|
}
|