XcodeGen/Sources/XcodeGenKit/PBXProjGenerator.swift

294 lines
13 KiB
Swift
Raw Normal View History

2017-07-23 21:58:03 +03:00
//
// PBXProjGenerator.swift
// XcodeGen
//
// Created by Yonas Kolb on 23/7/17.
//
//
import Foundation
import Foundation
import PathKit
import xcodeproj
import xcodeprojprotocols
import JSONUtilities
import Yams
public class PBXProjGenerator {
let spec: Spec
var objects: [PBXObject] = []
2017-07-24 00:12:14 +03:00
var fileReferencesByPath: [Path: String] = [:]
2017-07-25 00:26:45 +03:00
var groupsByPath: [String: PBXGroup] = [:]
2017-07-23 21:58:03 +03:00
var targetNativeReferences: [String: String] = [:]
var targetBuildFileReferences: [String: String] = [:]
var targetFileReferences: [String: String] = [:]
var topLevelGroups: [PBXGroup] = []
var ids = 0
var projectReference: String
2017-07-24 19:03:41 +03:00
var basePath: Path {
return spec.path.parent()
}
2017-07-23 21:58:03 +03:00
public init(spec: Spec) {
self.spec = spec
projectReference = ""
projectReference = id()
}
func id() -> String {
ids += 1
// return ids.description.md5().uppercased()
return "OBJECT_\(ids)"
}
public func generate() throws -> PBXProj {
let buildConfigs: [XCBuildConfiguration] = try spec.configs.map { config in
var buildSettings = config.buildSettings
if let type = config.type, let typeBuildSettings = try BuildSettingsPreset.config(type).getBuildSettings() {
buildSettings = typeBuildSettings.merged(buildSettings)
}
return XCBuildConfiguration(reference: id(), name: config.name, baseConfigurationReference: nil, buildSettings: buildSettings)
2017-07-23 21:58:03 +03:00
}
let buildConfigList = XCConfigurationList(reference: id(), buildConfigurations: buildConfigs.referenceSet, defaultConfigurationName: buildConfigs.first?.name ?? "", defaultConfigurationIsVisible: 0)
objects += buildConfigs.map { .xcBuildConfiguration($0) }
objects.append(.xcConfigurationList(buildConfigList))
for target in spec.targets {
targetNativeReferences[target.name] = id()
let fileReference = PBXFileReference(reference: id(), sourceTree: .buildProductsDir, explicitFileType: target.type.fileExtension, path: target.filename, includeInIndex: 0)
objects.append(.pbxFileReference(fileReference))
targetFileReferences[target.name] = fileReference.reference
let buildFile = PBXBuildFile(reference: id(), fileRef: fileReference.reference)
objects.append(.pbxBuildFile(buildFile))
targetBuildFileReferences[target.name] = buildFile.reference
}
2017-07-24 14:35:21 +03:00
let targets = try spec.targets.map(generateTarget)
2017-07-23 21:58:03 +03:00
2017-07-25 00:38:08 +03:00
let productGroup = PBXGroup(reference: id(), children: Array(targetFileReferences.values), sourceTree: .group, name: "Products")
2017-07-23 21:58:03 +03:00
objects.append(.pbxGroup(productGroup))
topLevelGroups.append(productGroup)
2017-07-25 00:38:08 +03:00
let mainGroup = PBXGroup(reference: id(), children: topLevelGroups.referenceList, sourceTree: .group)
2017-07-23 21:58:03 +03:00
objects.append(.pbxGroup(mainGroup))
2017-07-23 23:39:18 +03:00
let knownRegions: [String] = ["en", "Base"]
2017-07-24 14:35:21 +03:00
let pbxProjectRoot = PBXProject(reference: projectReference, buildConfigurationList: buildConfigList.reference, compatibilityVersion: "Xcode 3.2", mainGroup: mainGroup.reference, developmentRegion: "English", knownRegions: knownRegions, targets: targets.referenceList)
2017-07-23 21:58:03 +03:00
objects.append(.pbxProject(pbxProjectRoot))
return PBXProj(archiveVersion: 1, objectVersion: 46, rootObject: projectReference, objects: objects)
}
struct SourceFile {
let path: Path
2017-07-24 00:12:14 +03:00
let fileReference: String
2017-07-23 21:58:03 +03:00
let buildFile: PBXBuildFile
}
func generateSourceFile(path: Path) -> SourceFile {
let fileReference = fileReferencesByPath[path]!
var settings: [String: Any] = [:]
if getBuildPhaseForPath(path) == .headers {
settings["ATTRIBUTES"] = ["Public"]
}
2017-07-24 00:12:14 +03:00
let buildFile = PBXBuildFile(reference: id(), fileRef: fileReference, settings: settings)
2017-07-23 21:58:03 +03:00
objects.append(.pbxBuildFile(buildFile))
return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile)
}
2017-07-24 14:35:21 +03:00
func generateTarget(_ target: Target) throws -> PBXNativeTarget {
2017-07-24 19:03:41 +03:00
let source = basePath + target.sources.first!
2017-07-23 21:58:03 +03:00
//TODO: handle multiple sources
//TODO: handle targets with shared sources
2017-07-25 00:26:45 +03:00
let sourceGroups = try getGroups(path: source, groupReference: id())
let sourceFiles = sourceGroups.filePaths.map(generateSourceFile)
2017-07-24 19:03:41 +03:00
//TODO: don't generate build files for files that won't be built
2017-07-23 21:58:03 +03:00
let configs: [XCBuildConfiguration] = try spec.configs.map { config in
let buildSettings = try getTargetBuildSettings(config: config, target: target)
2017-07-24 19:03:41 +03:00
var baseConfigurationReference: String?
if let configPath = target.configs[config.name] {
let path = basePath + configPath
baseConfigurationReference = fileReferencesByPath[path]
}
return XCBuildConfiguration(reference: id(), name: config.name, baseConfigurationReference: baseConfigurationReference, buildSettings: buildSettings)
2017-07-23 21:58:03 +03:00
}
objects += configs.map { .xcBuildConfiguration($0) }
let buildConfigList = XCConfigurationList(reference: id(), buildConfigurations: configs.referenceSet, defaultConfigurationName: "")
objects.append(.xcConfigurationList(buildConfigList))
var dependancies: [String] = []
var frameworkFiles: [String] = []
var copyFiles: [String] = []
for dependancy in target.dependencies {
2017-07-24 13:46:01 +03:00
switch dependancy {
case let .target(dependencyTarget):
let targetProxy = PBXContainerItemProxy(reference: id(), containerPortal: projectReference, remoteGlobalIDString: targetNativeReferences[dependencyTarget]!, proxyType: .nativeTarget, remoteInfo: dependencyTarget)
let targetDependancy = PBXTargetDependency(reference: id(), target: targetNativeReferences[dependencyTarget]!, targetProxy: targetProxy.reference )
2017-07-23 21:58:03 +03:00
objects.append(.pbxContainerItemProxy(targetProxy))
objects.append(.pbxTargetDependency(targetDependancy))
dependancies.append(targetDependancy.reference)
2017-07-24 13:46:01 +03:00
let dependencyBuildFile = targetBuildFileReferences[dependencyTarget]!
2017-07-24 00:35:28 +03:00
//link
2017-07-23 21:58:03 +03:00
frameworkFiles.append(dependencyBuildFile)
2017-07-24 00:35:28 +03:00
//embed
let embedSettings: [String: Any] = ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]]
2017-07-24 13:46:01 +03:00
let embedFile = PBXBuildFile(reference: id(), fileRef: targetFileReferences[dependencyTarget]!, settings: embedSettings)
2017-07-24 00:35:28 +03:00
objects.append(.pbxBuildFile(embedFile))
copyFiles.append(embedFile.reference)
2017-07-23 21:58:03 +03:00
case .system:
//TODO: handle system frameworks
break
}
}
let fileReference = targetFileReferences[target.name]!
var buildPhases: [String] = []
if target.type == .framework {
let buildFile = PBXBuildFile(reference: targetBuildFileReferences[target.name]!, fileRef: fileReference)
objects.append(.pbxBuildFile(buildFile))
}
func getBuildFilesForPhase(_ buildPhase: BuildPhase) -> Set<String> {
let files = sourceFiles.filter { getBuildPhaseForPath($0.path) == buildPhase }
return Set(files.map { $0.buildFile.reference })
}
let sourcesBuildPhase = PBXSourcesBuildPhase(reference: id(), files: getBuildFilesForPhase(.sources))
objects.append(.pbxSourcesBuildPhase(sourcesBuildPhase))
buildPhases.append(sourcesBuildPhase.reference)
let resourcesBuildPhase = PBXResourcesBuildPhase(reference: id(), files: getBuildFilesForPhase(.resources))
objects.append(.pbxResourcesBuildPhase(resourcesBuildPhase))
buildPhases.append(resourcesBuildPhase.reference)
let headersBuildPhase = PBXHeadersBuildPhase(reference: id(), files: getBuildFilesForPhase(.headers))
objects.append(.pbxHeadersBuildPhase(headersBuildPhase))
buildPhases.append(headersBuildPhase.reference)
let frameworkBuildPhase = PBXFrameworksBuildPhase(reference: id(), files: Set(frameworkFiles), runOnlyForDeploymentPostprocessing: 0)
objects.append(.pbxFrameworksBuildPhase(frameworkBuildPhase))
buildPhases.append(frameworkBuildPhase.reference)
2017-07-24 00:35:28 +03:00
let copyFilesPhase = PBXCopyFilesBuildPhase(reference: id(), dstPath: "", dstSubfolderSpec: .frameworks, files: Set(copyFiles))
objects.append(.pbxCopyFilesBuildPhase(copyFilesPhase))
buildPhases.append(copyFilesPhase.reference)
2017-07-23 21:58:03 +03:00
let nativeTarget = PBXNativeTarget(
reference: targetNativeReferences[target.name]!,
buildConfigurationList: buildConfigList.reference,
buildPhases: buildPhases,
buildRules: [],
dependencies: dependancies,
name: target.name,
productReference: fileReference,
productType: target.type)
objects.append(.pbxNativeTarget(nativeTarget))
2017-07-24 14:35:21 +03:00
return nativeTarget
2017-07-23 21:58:03 +03:00
}
func getBuildPhaseForPath(_ path: Path) -> BuildPhase? {
if path.lastComponent == "Info.plist" {
return nil
}
2017-07-23 21:58:03 +03:00
if let fileExtension = path.extension {
switch fileExtension {
2017-07-23 22:25:02 +03:00
case "swift", "m": return .sources
2017-07-23 21:58:03 +03:00
case "h", "hh", "hpp", "ipp", "tpp", "hxx", "def": return .headers
case "xcconfig": return nil
2017-07-23 21:58:03 +03:00
default: return .resources
}
}
return nil
}
func getTargetBuildSettings(config: Config, target: Target) throws -> BuildSettings {
var buildSettings = BuildSettings()
func getBuildSettingPreset(_ type: BuildSettingsPreset) throws -> BuildSettings? {
return try type.getBuildSettings()
}
buildSettings += try getBuildSettingPreset(.base)
buildSettings += try getBuildSettingPreset(.platform(target.platform))
buildSettings += try getBuildSettingPreset(.product(target.type))
buildSettings += target.buildSettings?.buildSettings
buildSettings += target.buildSettings?.configSettings[config.name]
return buildSettings
}
2017-07-25 00:26:45 +03:00
func getGroups(path: Path, groupReference: String, depth: Int = 0) throws -> (filePaths: [Path], groups: [PBXGroup]) {
2017-07-23 21:58:03 +03:00
2017-07-24 00:12:14 +03:00
let directories = try path.children().filter { $0.isDirectory && $0.extension == nil && $0.extension != "lproj" }
var filePaths = try path.children().filter { $0.isFile || $0.extension != nil && $0.extension != "lproj" }
let localisedDirectories = try path.children().filter { $0.extension == "lproj" }
2017-07-23 21:58:03 +03:00
var groupChildren: [String] = []
2017-07-24 14:50:59 +03:00
var allFilePaths: [Path] = filePaths
2017-07-25 00:26:45 +03:00
var groups: [PBXGroup] = []
2017-07-23 21:58:03 +03:00
2017-07-24 14:26:58 +03:00
let childGroupReference = directories.map { _ in id() }
for (reference, path) in zip(childGroupReference,directories) {
2017-07-25 00:26:45 +03:00
let subGroups = try getGroups(path: path, groupReference: reference, depth: depth + 1)
allFilePaths += subGroups.filePaths
groupChildren.append(subGroups.groups.first!.reference)
groups += subGroups.groups
2017-07-24 14:26:58 +03:00
}
2017-07-23 21:58:03 +03:00
for path in filePaths {
if let fileReference = fileReferencesByPath[path] {
2017-07-24 00:12:14 +03:00
groupChildren.append(fileReference)
2017-07-23 21:58:03 +03:00
} else {
let fileReference = PBXFileReference(reference: id(), sourceTree: .group, path: path.lastComponent)
objects.append(.pbxFileReference(fileReference))
2017-07-24 00:12:14 +03:00
fileReferencesByPath[path] = fileReference.reference
2017-07-23 21:58:03 +03:00
groupChildren.append(fileReference.reference)
}
}
2017-07-24 00:12:14 +03:00
for localisedDirectory in localisedDirectories {
for path in try localisedDirectory.children() {
let filePath = "\(localisedDirectory.lastComponent)/\(path.lastComponent)"
let fileReference = PBXFileReference(reference: id(), sourceTree: .group, name: localisedDirectory.lastComponentWithoutExtension, path: filePath)
objects.append(.pbxFileReference(fileReference))
let variantGroup = PBXVariantGroup(reference: id(), children: Set([fileReference.reference]), name: path.lastComponent, sourceTree: .group)
objects.append(.pbxVariantGroup(variantGroup))
fileReferencesByPath[path] = variantGroup.reference
groupChildren.append(variantGroup.reference)
filePaths.append(path)
}
}
2017-07-24 19:03:41 +03:00
let groupPath: String = depth == 0 ? path.byRemovingBase(path: basePath).string : path.lastComponent
2017-07-25 00:26:45 +03:00
let group: PBXGroup
if let cachedGroup = groupsByPath[groupPath] {
group = cachedGroup
} else {
2017-07-25 00:38:08 +03:00
group = PBXGroup(reference: groupReference, children: groupChildren, sourceTree: .group, name: path.lastComponent, path: groupPath)
2017-07-25 00:26:45 +03:00
objects.append(.pbxGroup(group))
if depth == 0 {
topLevelGroups.append(group)
}
groupsByPath[groupPath] = group
}
groups.insert(group, at: 0)
return (allFilePaths, groups)
2017-07-23 21:58:03 +03:00
}
}