Files
XcodeGen/Sources/XcodeGenKit/PBXProjGenerator.swift
T
Keith Smiley e636664aaa Add defaultConfigurationName to options
This allows users to set the defaultConfigurationName project wide. This
setting corresponds to the drop down in the project settings that says
"Use CONFIG for command line builds". This affects which configuration
Xcode looks in for some settings, even if you pass `-configuration FOO`.
2018-03-19 13:47:02 -07:00

691 lines
27 KiB
Swift

import Foundation
import JSONUtilities
import PathKit
import ProjectSpec
import xcproj
import Yams
public class PBXProjGenerator {
let spec: ProjectSpec
let proj: PBXProj
var sourceGenerator: SourceGenerator!
var targetObjects: [String: ObjectReference<PBXTarget>] = [:]
var targetBuildFiles: [String: ObjectReference<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(rootObject: "", objectVersion: 46)
sourceGenerator = SourceGenerator(spec: spec) { [unowned self] id, object in
self.addObject(id: id, object)
}
}
func addObject(id: String, _ object: PBXObject) -> String {
let reference = proj.objects.generateReference(object, id)
proj.objects.addObject(object, reference: reference)
return reference
}
func createObject<T>(id: String, _ object: T) -> ObjectReference<T> {
let reference = addObject(id: id, object)
return ObjectReference(reference: reference, object: 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: [ObjectReference<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 createObject(
id: config.name,
XCBuildConfiguration(
name: config.name,
baseConfigurationReference: baseConfigurationReference,
buildSettings: buildSettings
)
)
}
let configName = spec.options.defaultConfigurationName ?? buildConfigs.first?.object.name ?? ""
let buildConfigList = createObject(
id: spec.name,
XCConfigurationList(
buildConfigurations: buildConfigs.map { $0.reference },
defaultConfigurationName: configName
)
)
let mainGroup = createObject(
id: "Project",
PBXGroup(
children: [],
sourceTree: .group,
usesTabs: spec.options.usesTabs,
indentWidth: spec.options.indentWidth,
tabWidth: spec.options.tabWidth
)
)
let project = createObject(
id: spec.name,
PBXProject(
name: spec.name,
buildConfigurationList: buildConfigList.reference,
compatibilityVersion: "Xcode 3.2",
mainGroup: mainGroup.reference,
developmentRegion: spec.options.developmentLanguage ?? "en"
)
)
proj.rootObject = project.reference
for target in spec.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
)
} else {
targetObject = PBXNativeTarget(name: target.name)
}
targetObjects[target.name] = createObject(id: target.name, targetObject)
var explicitFileType: String?
var lastKnownFileType: String?
let fileType = PBXFileReference.fileType(path: Path(target.filename))
if target.platform == .macOS || target.platform == .watchOS || target.type == .framework {
explicitFileType = fileType
} else {
lastKnownFileType = fileType
}
let fileReference = createObject(
id: target.name,
PBXFileReference(
sourceTree: .buildProductsDir,
explicitFileType: explicitFileType,
lastKnownFileType: lastKnownFileType,
path: target.filename,
includeInIndex: false
)
)
targetFileReferences[target.name] = fileReference.reference
targetBuildFiles[target.name] = createObject(
id: fileReference.reference,
PBXBuildFile(fileRef: fileReference.reference)
)
}
try spec.targets.forEach(generateTarget)
let productGroup = createObject(
id: "Products",
PBXGroup(
children: Array(targetFileReferences.values),
sourceTree: .group,
name: "Products"
)
)
topLevelGroups.insert(productGroup.reference)
if !carthageFrameworksByPlatform.isEmpty {
var platforms: [PBXGroup] = []
var platformReferences: [String] = []
for (platform, fileReferences) in carthageFrameworksByPlatform {
let platformGroup: ObjectReference<PBXGroup> = createObject(
id: "Carthage" + platform,
PBXGroup(
children: fileReferences.sorted(),
sourceTree: .group,
path: platform
)
)
platformReferences.append(platformGroup.reference)
platforms.append(platformGroup.object)
}
let carthageGroup = createObject(
id: "Carthage",
PBXGroup(
children: platformReferences.sorted(),
sourceTree: .group,
name: "Carthage",
path: carthageBuildPath
)
)
frameworkFiles.append(carthageGroup.reference)
}
if !frameworkFiles.isEmpty {
let group = createObject(
id: "Frameworks",
PBXGroup(
children: frameworkFiles,
sourceTree: .group,
name: "Frameworks"
)
)
topLevelGroups.insert(group.reference)
}
for rootGroup in sourceGenerator.rootGroups {
topLevelGroups.insert(rootGroup)
}
mainGroup.object.children = Array(topLevelGroups)
sortGroups(group: mainGroup)
let projectAttributes: [String: Any] = ["LastUpgradeCheck": spec.xcodeVersion]
.merged(spec.attributes)
.merged(generateTargetAttributes() ?? [:])
project.object.knownRegions = sourceGenerator.knownRegions.sorted()
project.object.targets = targetObjects.values.sorted { $0.object.name < $1.object.name }.map { $0.reference }
project.object.attributes = projectAttributes
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 = proj.objects.nativeTargets.objectReferences.filter { $0.object.productType == .uiTestBundle }
for uiTestTarget in uiTestTargets {
guard let name = testTargetName(uiTestTarget.object) 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: ObjectReference<PBXGroup>) {
// sort children
let children = group.object.children
.flatMap { reference -> ObjectReference<PBXFileElement>? in
guard let fileElement = proj.objects.getFileElement(reference: reference) else {
return nil
}
return ObjectReference(reference: reference, object: fileElement)
}
.sorted { child1, child2 in
if child1.object.sortOrder == child2.object.sortOrder {
return child1.object.nameOrPath < child2.object.nameOrPath
} else {
return child1.object.sortOrder < child2.object.sortOrder
}
}
group.object.children = children.map { $0.reference }.filter { $0 != group.reference }
// sort sub groups
let childGroups = group.object.children.flatMap { reference -> ObjectReference<PBXGroup>? in
guard let group = proj.objects.groups[reference] else {
return nil
}
return ObjectReference(reference: reference, object: group) }
childGroups.forEach(sortGroups)
}
func generateTarget(_ target: Target) throws {
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: [ObjectReference<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)
}
let buildConfig = XCBuildConfiguration(
name: config.name,
baseConfigurationReference: baseConfigurationReference,
buildSettings: buildSettings
)
return createObject(id: config.name + target.name, buildConfig)
}
let buildConfigList = createObject(id: target.name, XCConfigurationList(
buildConfigurations: configs.map { $0.reference },
defaultConfigurationName: ""
))
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.shouldEmbedDependencies
switch dependency.type {
case .target:
let dependencyTargetName = dependency.reference
guard let dependencyTarget = spec.getTarget(dependencyTargetName) else { continue }
let dependencyFileReference = targetFileReferences[dependencyTargetName]!
let targetProxy = createObject(
id: target.name,
PBXContainerItemProxy(
containerPortal: proj.rootObject,
remoteGlobalIDString: targetObjects[dependencyTargetName]!.reference,
proxyType: .nativeTarget,
remoteInfo: dependencyTargetName
)
)
let targetDependency = createObject(
id: dependencyTargetName + target.name,
PBXTargetDependency(
target: targetObjects[dependencyTargetName]!.reference,
targetProxy: targetProxy.reference
)
)
dependencies.append(targetDependency.reference)
if (dependencyTarget.type.isLibrary || dependencyTarget.type.isFramework) && dependency.link {
let dependencyBuildFile = targetBuildFiles[dependencyTargetName]!
let buildFile = createObject(
id: dependencyBuildFile.reference + target.name,
PBXBuildFile(fileRef: dependencyBuildFile.object.fileRef!)
)
targetFrameworkBuildFiles.append(buildFile.reference)
}
if (dependency.embed ?? target.type.isApp) && !dependencyTarget.type.isLibrary {
let embedFile = createObject(
id: dependencyFileReference + target.name,
PBXBuildFile(
fileRef: dependencyFileReference,
settings: dependency.buildSettings
)
)
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 = createObject(
id: fileReference + target.name,
PBXBuildFile(fileRef: fileReference)
)
targetFrameworkBuildFiles.append(buildFile.reference)
if !frameworkFiles.contains(fileReference) {
frameworkFiles.append(fileReference)
}
if embed {
let embedFile = createObject(
id: fileReference + target.name,
PBXBuildFile(fileRef: fileReference, settings: dependency.buildSettings)
)
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 = createObject(
id: fileReference + target.name,
PBXBuildFile(fileRef: fileReference)
)
carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference)
targetFrameworkBuildFiles.append(buildFile.reference)
if target.platform == .macOS && embed {
let embedFile = createObject(
id: fileReference + target.name,
PBXBuildFile(fileRef: fileReference, settings: dependency.buildSettings)
)
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 }
return files.map { createObject(id: $0.fileReference + target.name, $0.buildFile) }
.map { $0.reference }
}
func generateBuildScript(buildScript: BuildScript) throws {
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(
files: [],
name: buildScript.name ?? "Run Script",
inputPaths: buildScript.inputFiles,
outputPaths: buildScript.outputFiles,
shellPath: buildScript.shell ?? "/bin/sh",
shellScript: shellScript
)
shellScriptPhase.runOnlyForDeploymentPostprocessing = buildScript.runOnlyWhenInstalling
let shellScriptPhaseReference = createObject(id: String(describing: buildScript.name) + shellScript + target.name, shellScriptPhase)
buildPhases.append(shellScriptPhaseReference.reference)
}
try target.prebuildScripts.forEach(generateBuildScript)
let sourcesBuildPhaseFiles = getBuildFilesForPhase(.sources)
if !sourcesBuildPhaseFiles.isEmpty {
let sourcesBuildPhase = createObject(id: target.name, PBXSourcesBuildPhase(files: sourcesBuildPhaseFiles))
buildPhases.append(sourcesBuildPhase.reference)
}
let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences
if !resourcesBuildPhaseFiles.isEmpty {
let resourcesBuildPhase = createObject(id: target.name, PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles))
buildPhases.append(resourcesBuildPhase.reference)
}
let headersBuildPhaseFiles = getBuildFilesForPhase(.headers)
if !headersBuildPhaseFiles.isEmpty && (target.type == .framework || target.type == .dynamicLibrary) {
let headersBuildPhase = createObject(id: target.name, PBXHeadersBuildPhase(files: headersBuildPhaseFiles))
buildPhases.append(headersBuildPhase.reference)
}
if !targetFrameworkBuildFiles.isEmpty {
let frameworkBuildPhase = createObject(
id: target.name,
PBXFrameworksBuildPhase(files: targetFrameworkBuildFiles)
)
buildPhases.append(frameworkBuildPhase.reference)
}
if !extensions.isEmpty {
let copyFilesPhase = createObject(
id: "embed app extensions" + target.name,
PBXCopyFilesBuildPhase(
dstPath: "",
dstSubfolderSpec: .plugins,
files: extensions
)
)
buildPhases.append(copyFilesPhase.reference)
}
if !copyFrameworksReferences.isEmpty {
let copyFilesPhase = createObject(
id: "embed frameworks" + target.name,
PBXCopyFilesBuildPhase(
dstPath: "",
dstSubfolderSpec: .frameworks,
files: copyFrameworksReferences
)
)
buildPhases.append(copyFilesPhase.reference)
}
if !copyWatchReferences.isEmpty {
let copyFilesPhase = createObject(
id: "embed watch content" + target.name,
PBXCopyFilesBuildPhase(
dstPath: "$(CONTENTS_FOLDER_PATH)/Watch",
dstSubfolderSpec: .productsDirectory,
files: copyWatchReferences
)
)
buildPhases.append(copyFilesPhase.reference)
}
let carthageFrameworksToEmbed = Array(Set(
carthageDependencies
.filter { $0.embed ?? target.shouldEmbedDependencies }
.map { $0.reference }
))
.sorted()
if !carthageFrameworksToEmbed.isEmpty && 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 carthageExecutable = spec.options.carthageExecutablePath ?? "carthage"
let carthageScript = createObject(
id: "Carthage" + target.name,
PBXShellScriptBuildPhase(
files: [],
name: "Carthage",
inputPaths: inputPaths,
outputPaths: outputPaths,
shellPath: "/bin/sh",
shellScript: "\(carthageExecutable) copy-frameworks\n"
)
)
buildPhases.append(carthageScript.reference)
}
try target.postbuildScripts.forEach(generateBuildScript)
let targetObject = targetObjects[target.name]!.object
targetObject.name = target.name
targetObject.buildConfigurationList = buildConfigList.reference
targetObject.buildPhases = buildPhases
targetObject.dependencies = dependencies
targetObject.productName = target.name
targetObject.productReference = fileReference
if !target.isLegacy {
targetObject.productType = target.type
}
}
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
}
}
extension Target {
var shouldEmbedDependencies: Bool {
return type.isApp || type.isTest
}
}