Files
XcodeGen/Sources/XcodeGenKit/PBXProjGenerator.swift

679 lines
28 KiB
Swift

import Foundation
import PathKit
import xcproj
import JSONUtilities
import Yams
import ProjectSpec
public class PBXProjGenerator {
let spec: ProjectSpec
let proj: PBXProj
let sourceGenerator: SourceGenerator
let referenceGenerator = ReferenceGenerator()
var targetNativeReferences: [String: String] = [:]
var targetBuildFiles: [String: PBXBuildFile] = [:]
var targetFileReferences: [String: String] = [:]
var topLevelGroups: Set<String> = []
var carthageFrameworksByPlatform: [String: Set<String>] = [:]
var frameworkFiles: [String] = []
var generated = false
var carthageBuildPath: String {
return spec.options.carthageBuildPath ?? "Carthage/Build"
}
public init(spec: ProjectSpec) {
self.spec = spec
proj = PBXProj(objectVersion: 46, rootObject: referenceGenerator.generate(PBXProject.self, spec.name))
sourceGenerator = SourceGenerator(spec: spec, referenceGenerator: referenceGenerator) { _ in }
sourceGenerator.addObject = { [weak self] object in
self?.addObject(object)
}
}
func addObject(_ object: PBXObject) {
proj.objects.addObject(object)
}
public func generate() throws -> PBXProj {
if generated {
fatalError("Cannot use PBXProjGenerator to generate more than once")
}
generated = true
for group in spec.fileGroups {
try sourceGenerator.getFileGroups(path: 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 = sourceGenerator.getContainedFileReference(path: spec.basePath + configPath)
}
return XCBuildConfiguration(
reference: referenceGenerator.generate(XCBuildConfiguration.self, config.name),
name: config.name,
baseConfigurationReference: baseConfigurationReference,
buildSettings: buildSettings
)
}
let buildConfigList = XCConfigurationList(
reference: referenceGenerator.generate(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] = target.isLegacy ?
referenceGenerator.generate(PBXLegacyTarget.self, target.name) :
referenceGenerator.generate(PBXNativeTarget.self, target.name)
var explicitFileType: String?
var lastKnownFileType: String?
let fileType = PBXFileReference.fileType(path: Path(target.filename))
if (target.platform == .macOS || target.type == .framework) {
explicitFileType = fileType
} else {
lastKnownFileType = fileType
}
let fileReference = PBXFileReference(
reference: referenceGenerator.generate(PBXFileReference.self, target.name),
sourceTree: .buildProductsDir,
explicitFileType: explicitFileType,
lastKnownFileType: lastKnownFileType,
path: target.filename,
includeInIndex: 0
)
addObject(fileReference)
targetFileReferences[target.name] = fileReference.reference
let buildFile = PBXBuildFile(
reference: referenceGenerator.generate(PBXBuildFile.self, fileReference.reference),
fileRef: fileReference.reference
)
addObject(buildFile)
targetBuildFiles[target.name] = buildFile
}
let targets = try spec.targets.map(generateTarget)
let productGroup = PBXGroup(
reference: referenceGenerator.generate(PBXGroup.self, "Products"),
children: Array(targetFileReferences.values),
sourceTree: .group,
name: "Products"
)
addObject(productGroup)
topLevelGroups.insert(productGroup.reference)
if !carthageFrameworksByPlatform.isEmpty {
var platforms: [PBXGroup] = []
for (platform, fileReferences) in carthageFrameworksByPlatform {
let platformGroup = PBXGroup(
reference: referenceGenerator.generate(PBXGroup.self, "Carthage" + platform),
children: fileReferences.sorted(),
sourceTree: .group,
path: platform
)
addObject(platformGroup)
platforms.append(platformGroup)
}
let carthageGroup = PBXGroup(
reference: referenceGenerator.generate(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: referenceGenerator.generate(PBXGroup.self, "Frameworks"),
children: frameworkFiles,
sourceTree: .group,
name: "Frameworks"
)
addObject(group)
topLevelGroups.insert(group.reference)
}
for rootGroup in sourceGenerator.rootGroups {
topLevelGroups.insert(rootGroup)
}
let mainGroup = PBXGroup(
reference: referenceGenerator.generate(PBXGroup.self, "Project"),
children: Array(topLevelGroups),
sourceTree: .group,
usesTabs: spec.options.usesTabs.map { $0 ? 1 : 0 },
indentWidth: spec.options.indentWidth,
tabWidth: spec.options.tabWidth
)
addObject(mainGroup)
sortGroups(group: mainGroup)
let projectAttributes: [String: Any] = ["LastUpgradeCheck": spec.xcodeVersion]
.merged(spec.attributes)
.merged(self.generateTargetAttributes() ?? [:])
let root = PBXProject(
name: spec.name,
reference: proj.rootObject,
buildConfigurationList: buildConfigList.reference,
compatibilityVersion: "Xcode 3.2",
mainGroup: mainGroup.reference,
developmentRegion: spec.options.developmentLanguage ?? "en",
knownRegions: sourceGenerator.knownRegions.sorted(),
targets: targets.references,
attributes: projectAttributes
)
proj.objects.projects.append(root)
return proj
}
func generateTargetAttributes() -> [String: Any]? {
var targetAttributes: [String: Any] = [:]
// look up TEST_TARGET_NAME build setting
func testTargetName(_ target: PBXTarget) -> String? {
guard let configurationList = target.buildConfigurationList else { return nil }
guard let buildConfigurationReferences = self.proj.objects.configurationLists[configurationList]?.buildConfigurations else { return nil }
let configs = buildConfigurationReferences
.flatMap { ref in self.proj.objects.buildConfigurations[ref] }
return configs
.flatMap { $0.buildSettings["TEST_TARGET_NAME"] as? String }
.first
}
let uiTestTargets = self.proj.objects.nativeTargets.values
.filter { $0.productType == .uiTestBundle }
for uiTestTarget in uiTestTargets {
guard let name = testTargetName(uiTestTarget) else { continue }
guard let target = self.proj.objects.targets(named: name).first else { continue }
targetAttributes[uiTestTarget.reference] = [ "TestTargetID": target.reference]
}
guard !targetAttributes.isEmpty else { return nil }
return [
"TargetAttributes": targetAttributes
]
}
func sortGroups(group: PBXGroup) {
// sort children
let children = group.children
.flatMap { proj.objects.getFileElement(reference: $0) }
.sorted { child1, child2 in
if child1.sortOrder == child2.sortOrder {
return child1.nameOrPath < child2.nameOrPath
} else {
return child1.sortOrder < child2.sortOrder
}
}
group.children = children.map { $0.reference }.filter { $0 != group.reference }
// sort sub groups
let childGroups = group.children.flatMap { proj.objects.groups[$0] }
childGroups.forEach(sortGroups)
}
func generateTarget(_ target: Target) throws -> PBXTarget {
sourceGenerator.targetName = target.name
let carthageDependencies = getAllCarthageDependencies(target: target)
let sourceFiles = try sourceGenerator.getAllSourceFiles(sources: target.sources)
var plistPath: Path?
var searchForPlist = true
let configs: [XCBuildConfiguration] = spec.configs.map { config in
var buildSettings = spec.getTargetBuildSettings(target: target, config: config)
// automatically set INFOPLIST_FILE path
if !spec.targetHasBuildSetting("INFOPLIST_FILE", basePath: spec.basePath, target: target, config: config) {
if searchForPlist {
plistPath = getInfoPlist(target.sources)
searchForPlist = false
}
if let plistPath = plistPath {
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 = sourceGenerator.getContainedFileReference(path: spec.basePath + configPath)
}
return XCBuildConfiguration(
reference: referenceGenerator.generate(XCBuildConfiguration.self, config.name + target.name),
name: config.name,
baseConfigurationReference: baseConfigurationReference,
buildSettings: buildSettings
)
}
configs.forEach(addObject)
let buildConfigList = XCConfigurationList(
reference: referenceGenerator.generate(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.shouldEmbedDynamicFrameworks ? 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: referenceGenerator.generate(PBXContainerItemProxy.self, target.name),
containerPortal: proj.rootObject,
remoteGlobalIDString: targetNativeReferences[dependencyTargetName]!,
proxyType: .nativeTarget,
remoteInfo: dependencyTargetName
)
let targetDependency = PBXTargetDependency(
reference: referenceGenerator.generate(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: referenceGenerator.generate(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: referenceGenerator.generate(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: String
if dependency.implicit {
fileReference = sourceGenerator.getFileReference(
path: Path(dependency.reference),
inPath: spec.basePath,
sourceTree: .buildProductsDir
)
} else {
fileReference = sourceGenerator.getFileReference(
path: Path(dependency.reference),
inPath: spec.basePath
)
}
let buildFile = PBXBuildFile(
reference: referenceGenerator.generate(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: referenceGenerator.generate(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 = sourceGenerator.getFileReference(path: frameworkPath, inPath: platformPath)
let buildFile = PBXBuildFile(
reference: referenceGenerator.generate(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: referenceGenerator.generate(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 { $0.buildPhase == buildPhase }
.reduce(into: [SourceFile]()) { (output, sourceFile) in
if !output.contains(where: { $0.fileReference == sourceFile.fileReference }) {
output.append(sourceFile)
}
}
.sorted { $0.path.lastComponent < $1.path.lastComponent }
files.forEach { addObject($0.buildFile) }
return files.map { $0.buildFile.reference }
}
func getBuildScript(buildScript: BuildScript) throws -> PBXShellScriptBuildPhase {
let shellScript: String
switch buildScript.script {
case let .path(path):
shellScript = try (spec.basePath + path).read()
case let .script(script):
shellScript = script
}
let shellScriptPhase = PBXShellScriptBuildPhase(
reference: referenceGenerator.generate(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 sourcesBuildPhaseFiles = getBuildFilesForPhase(.sources)
if !sourcesBuildPhaseFiles.isEmpty {
let sourcesBuildPhase = PBXSourcesBuildPhase(reference: referenceGenerator.generate(PBXSourcesBuildPhase.self, target.name), files: sourcesBuildPhaseFiles)
addObject(sourcesBuildPhase)
buildPhases.append(sourcesBuildPhase.reference)
}
let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences
if !resourcesBuildPhaseFiles.isEmpty {
let resourcesBuildPhase = PBXResourcesBuildPhase(reference: referenceGenerator.generate(PBXResourcesBuildPhase.self, target.name), files: resourcesBuildPhaseFiles)
addObject(resourcesBuildPhase)
buildPhases.append(resourcesBuildPhase.reference)
}
let headersBuildPhaseFiles = getBuildFilesForPhase(.headers)
if !headersBuildPhaseFiles.isEmpty && (target.type == .framework || target.type == .dynamicLibrary) {
let headersBuildPhase = PBXHeadersBuildPhase(reference: referenceGenerator.generate(PBXHeadersBuildPhase.self, target.name), files: headersBuildPhaseFiles)
addObject(headersBuildPhase)
buildPhases.append(headersBuildPhase.reference)
}
if !targetFrameworkBuildFiles.isEmpty {
let frameworkBuildPhase = PBXFrameworksBuildPhase(
reference: referenceGenerator.generate(PBXFrameworksBuildPhase.self, target.name),
files: targetFrameworkBuildFiles,
runOnlyForDeploymentPostprocessing: 0
)
addObject(frameworkBuildPhase)
buildPhases.append(frameworkBuildPhase.reference)
}
if !extensions.isEmpty {
let copyFilesPhase = PBXCopyFilesBuildPhase(
reference: referenceGenerator.generate(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: referenceGenerator.generate(PBXCopyFilesBuildPhase.self, "embed frameworks" + target.name),
dstPath: "",
dstSubfolderSpec: .frameworks,
files: copyFrameworksReferences
)
addObject(copyFilesPhase)
buildPhases.append(copyFilesPhase.reference)
}
if !copyWatchReferences.isEmpty {
let copyFilesPhase = PBXCopyFilesBuildPhase(
reference: referenceGenerator.generate(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.shouldEmbedDynamicFrameworks && 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: referenceGenerator.generate(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 pbxtarget: PBXTarget
if target.isLegacy {
pbxtarget = PBXLegacyTarget(
reference: targetNativeReferences[target.name]!,
name: target.name,
buildToolPath: target.legacy?.toolPath,
buildArgumentsString: target.legacy?.arguments,
passBuildSettingsInEnvironment: target.legacy?.passSettings ?? false,
buildWorkingDirectory: target.legacy?.workingDirectory,
buildConfigurationList: buildConfigList.reference,
buildPhases: buildPhases,
buildRules: [],
dependencies: dependencies,
productName: target.name,
productReference: fileReference,
productType: nil
)
} else {
pbxtarget = PBXNativeTarget(
reference: targetNativeReferences[target.name]!,
name: target.name,
buildConfigurationList: buildConfigList.reference,
buildPhases: buildPhases,
buildRules: [],
dependencies: dependencies,
productName: target.name,
productReference: fileReference,
productType: target.type
)
}
addObject(pbxtarget)
return pbxtarget
}
func getInfoPlist(_ sources: [TargetSource]) -> Path? {
return sources
.lazy
.map { self.spec.basePath + $0.path }
.flatMap { (path) -> Path? in
if path.isFile {
return path.lastComponent == "Info.plist" ? path : nil
} else {
return path.first(where: { $0.lastComponent == "Info.plist" })
}
}
.first
}
func getCarthageBuildPath(platform: Platform) -> String {
let carthagePath = Path(carthageBuildPath)
let platformName = platform.carthageDirectoryName
return "\(carthagePath)/\(platformName)"
}
func getAllCarthageDependencies(target: Target, visitedTargets: [String: Bool] = [:]) -> [Dependency] {
// this is used to resolve cyclical target dependencies
var visitedTargets = visitedTargets
visitedTargets[target.name] = true
var frameworks: [Dependency] = []
for dependency in target.dependencies {
switch dependency.type {
case .carthage:
frameworks.append(dependency)
case .target:
let targetName = dependency.reference
if visitedTargets[targetName] == true {
return []
}
if let target = spec.getTarget(targetName) {
frameworks += getAllCarthageDependencies(target: target, visitedTargets: visitedTargets)
}
default: break
}
}
return frameworks
}
}