mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
e6a0af79a8
Copy Files phases don't stop dependent targets from starting to compile. If some of the files being copied are needed for the dependent target to compile then they need to happen before the Compile Sources phase. This change allows specifying that.
1048 lines
44 KiB
Swift
1048 lines
44 KiB
Swift
import Foundation
|
|
import JSONUtilities
|
|
import PathKit
|
|
import ProjectSpec
|
|
import xcproj
|
|
import Yams
|
|
|
|
public class PBXProjGenerator {
|
|
|
|
let project: Project
|
|
|
|
let pbxProj: PBXProj
|
|
var sourceGenerator: SourceGenerator!
|
|
|
|
var targetObjects: [String: ObjectReference<PBXTarget>] = [:]
|
|
var targetAggregateObjects: [String: ObjectReference<PBXAggregateTarget>] = [:]
|
|
var targetBuildFiles: [String: ObjectReference<PBXBuildFile>] = [:]
|
|
var targetFileReferences: [String: String] = [:]
|
|
|
|
var carthageFrameworksByPlatform: [String: Set<String>] = [:]
|
|
var frameworkFiles: [String] = []
|
|
|
|
var generated = false
|
|
|
|
var carthageBuildPath: String {
|
|
return project.options.carthageBuildPath ?? "Carthage/Build"
|
|
}
|
|
|
|
public init(project: Project) {
|
|
self.project = project
|
|
pbxProj = PBXProj(rootObject: "", objectVersion: 46)
|
|
sourceGenerator = SourceGenerator(project: project) { [unowned self] id, object in
|
|
self.addObject(id: id, object)
|
|
}
|
|
}
|
|
|
|
func addObject(id: String, _ object: PBXObject) -> String {
|
|
let reference = pbxProj.objects.generateReference(object, id)
|
|
pbxProj.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 project.fileGroups {
|
|
try sourceGenerator.getFileGroups(path: group)
|
|
}
|
|
|
|
let buildConfigs: [ObjectReference<XCBuildConfiguration>] = project.configs.map { config in
|
|
let buildSettings = project.getProjectBuildSettings(config: config)
|
|
var baseConfigurationReference: String?
|
|
if let configPath = project.configFiles[config.name] {
|
|
baseConfigurationReference = sourceGenerator.getContainedFileReference(path: project.basePath + configPath)
|
|
}
|
|
return createObject(
|
|
id: config.name,
|
|
XCBuildConfiguration(
|
|
name: config.name,
|
|
baseConfigurationReference: baseConfigurationReference,
|
|
buildSettings: buildSettings
|
|
)
|
|
)
|
|
}
|
|
|
|
let configName = project.options.defaultConfig ?? buildConfigs.first?.object.name ?? ""
|
|
let buildConfigList = createObject(
|
|
id: project.name,
|
|
XCConfigurationList(
|
|
buildConfigurations: buildConfigs.map { $0.reference },
|
|
defaultConfigurationName: configName
|
|
)
|
|
)
|
|
|
|
var derivedGroups: [ObjectReference<PBXGroup>] = []
|
|
|
|
let mainGroup = createObject(
|
|
id: "Project",
|
|
PBXGroup(
|
|
children: [],
|
|
sourceTree: .group,
|
|
usesTabs: project.options.usesTabs,
|
|
indentWidth: project.options.indentWidth,
|
|
tabWidth: project.options.tabWidth
|
|
)
|
|
)
|
|
|
|
let pbxProject = createObject(
|
|
id: project.name,
|
|
PBXProject(
|
|
name: project.name,
|
|
buildConfigurationList: buildConfigList.reference,
|
|
compatibilityVersion: "Xcode 3.2",
|
|
mainGroup: mainGroup.reference,
|
|
developmentRegion: project.options.developmentLanguage ?? "en"
|
|
)
|
|
)
|
|
|
|
pbxProj.rootObject = pbxProject.reference
|
|
|
|
for target in project.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
|
|
}
|
|
|
|
if !target.isLegacy {
|
|
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)
|
|
)
|
|
}
|
|
}
|
|
|
|
for target in project.aggregateTargets {
|
|
|
|
let aggregateTarget = createObject(
|
|
id: target.name,
|
|
PBXAggregateTarget(
|
|
name: target.name,
|
|
productName: target.name
|
|
)
|
|
)
|
|
targetAggregateObjects[target.name] = aggregateTarget
|
|
}
|
|
|
|
try project.targets.forEach(generateTarget)
|
|
try project.aggregateTargets.forEach(generateAggregateTarget)
|
|
|
|
let productGroup = createObject(
|
|
id: "Products",
|
|
PBXGroup(
|
|
children: Array(targetFileReferences.values),
|
|
sourceTree: .group,
|
|
name: "Products"
|
|
)
|
|
)
|
|
derivedGroups.append(productGroup)
|
|
|
|
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"
|
|
)
|
|
)
|
|
derivedGroups.append(group)
|
|
}
|
|
|
|
mainGroup.object.children = Array(sourceGenerator.rootGroups)
|
|
sortGroups(group: mainGroup)
|
|
// add derived groups at the end
|
|
derivedGroups.forEach(sortGroups)
|
|
mainGroup.object.children += derivedGroups
|
|
.sorted { $0.object.nameOrPath.localizedStandardCompare($1.object.nameOrPath) == .orderedAscending }
|
|
.map { $0.reference }
|
|
|
|
let projectAttributes: [String: Any] = ["LastUpgradeCheck": project.xcodeVersion]
|
|
.merged(project.attributes)
|
|
.merged(generateTargetAttributes() ?? [:])
|
|
|
|
let knownRegions = sourceGenerator.knownRegions.sorted()
|
|
pbxProject.object.knownRegions = knownRegions.isEmpty ? ["en"] : knownRegions
|
|
|
|
let allTargets: [ObjectReference<PBXTarget>] = Array(targetObjects.values) + Array(targetAggregateObjects.values.map { ObjectReference(reference: $0.reference, object: $0.object) })
|
|
pbxProject.object.targets = allTargets
|
|
.sorted { $0.object.name < $1.object.name }
|
|
.map { $0.reference }
|
|
pbxProject.object.attributes = projectAttributes
|
|
|
|
return pbxProj
|
|
}
|
|
|
|
func generateAggregateTarget(_ target: AggregateTarget) throws {
|
|
|
|
let aggregateTarget = targetAggregateObjects[target.name]!.object
|
|
|
|
let configs: [ObjectReference<XCBuildConfiguration>] = project.configs.map { config in
|
|
|
|
let buildSettings = project.getBuildSettings(settings: target.settings, config: config)
|
|
|
|
var baseConfigurationReference: String?
|
|
if let configPath = target.configFiles[config.name] {
|
|
baseConfigurationReference = sourceGenerator.getContainedFileReference(path: project.basePath + configPath)
|
|
}
|
|
let buildConfig = XCBuildConfiguration(
|
|
name: config.name,
|
|
baseConfigurationReference: baseConfigurationReference,
|
|
buildSettings: buildSettings
|
|
)
|
|
return createObject(id: config.name + target.name, buildConfig)
|
|
}
|
|
|
|
let dependencies: [String] = target.targets.map { generateTargetDependency(from: target.name, to: $0).reference }
|
|
|
|
let buildConfigList = createObject(id: target.name, XCConfigurationList(
|
|
buildConfigurations: configs.map { $0.reference },
|
|
defaultConfigurationName: ""
|
|
))
|
|
|
|
var buildPhases: [String] = []
|
|
buildPhases += try target.buildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) }
|
|
|
|
aggregateTarget.buildPhases = buildPhases
|
|
aggregateTarget.buildConfigurationList = buildConfigList.reference
|
|
aggregateTarget.dependencies = dependencies
|
|
}
|
|
|
|
func generateTargetDependency(from: String, to target: String) -> ObjectReference<PBXTargetDependency> {
|
|
guard let targetReference = targetObjects[target]?.reference ?? targetAggregateObjects[target]?.reference else {
|
|
fatalError("target not found")
|
|
}
|
|
let targetProxy = createObject(
|
|
id: "\(from)-\(target)",
|
|
PBXContainerItemProxy(
|
|
containerPortal: pbxProj.rootObject,
|
|
remoteGlobalIDString: targetReference,
|
|
proxyType: .nativeTarget,
|
|
remoteInfo: target
|
|
)
|
|
)
|
|
|
|
let targetDependency = createObject(
|
|
id: "\(from)-\(target)",
|
|
PBXTargetDependency(
|
|
target: targetReference,
|
|
targetProxy: targetProxy.reference
|
|
)
|
|
)
|
|
return targetDependency
|
|
}
|
|
|
|
func generateBuildScript(targetName: String, buildScript: BuildScript) throws -> String {
|
|
|
|
let shellScript: String
|
|
switch buildScript.script {
|
|
case let .path(path):
|
|
shellScript = try (project.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
|
|
shellScriptPhase.showEnvVarsInLog = buildScript.showEnvVars
|
|
return createObject(id: String(describing: buildScript.name) + shellScript + targetName, shellScriptPhase).reference
|
|
}
|
|
|
|
func generateCopyFiles(targetName: String, copyFiles: TargetSource.BuildPhase.CopyFilesSettings, buildPhaseFiles: [String]) -> String {
|
|
return createObject(
|
|
id: "copy files" + copyFiles.destination.rawValue + copyFiles.subpath + targetName,
|
|
PBXCopyFilesBuildPhase(
|
|
dstPath: copyFiles.subpath,
|
|
dstSubfolderSpec: copyFiles.destination.destination,
|
|
files: buildPhaseFiles
|
|
)
|
|
).reference
|
|
}
|
|
|
|
func generateTargetAttributes() -> [String: Any]? {
|
|
|
|
var targetAttributes: [String: [String: Any]] = [:]
|
|
|
|
let uiTestTargets = pbxProj.objects.nativeTargets.objectReferences.filter { $0.object.productType == .uiTestBundle }
|
|
for uiTestTarget in uiTestTargets {
|
|
|
|
// look up TEST_TARGET_NAME build setting
|
|
func testTargetName(_ target: PBXTarget) -> String? {
|
|
guard let configurationList = target.buildConfigurationList else { return nil }
|
|
guard let buildConfigurationReferences = self.pbxProj.objects.configurationLists[configurationList]?.buildConfigurations else { return nil }
|
|
|
|
let configs = buildConfigurationReferences
|
|
.compactMap { ref in self.pbxProj.objects.buildConfigurations[ref] }
|
|
|
|
return configs
|
|
.compactMap { $0.buildSettings["TEST_TARGET_NAME"] as? String }
|
|
.first
|
|
}
|
|
|
|
guard let name = testTargetName(uiTestTarget.object) else { continue }
|
|
guard let target = self.pbxProj.objects.targets(named: name).first else { continue }
|
|
|
|
targetAttributes[uiTestTarget.reference, default: [:]].merge(["TestTargetID": target.reference])
|
|
}
|
|
|
|
func generateTargetAttributes(_ target: ProjectTarget, targetReference: String) {
|
|
if !target.attributes.isEmpty {
|
|
targetAttributes[targetReference, default: [:]].merge(target.attributes)
|
|
}
|
|
|
|
func getSingleBuildSetting(_ setting: String) -> String? {
|
|
let settings = project.configs.compactMap {
|
|
project.getCombinedBuildSettings(basePath: project.basePath, target: target, config: $0)[setting] as? String
|
|
}
|
|
guard settings.count == project.configs.count,
|
|
let firstSetting = settings.first,
|
|
settings.filter({ $0 == firstSetting }).count == settings.count else {
|
|
return nil
|
|
}
|
|
return firstSetting
|
|
}
|
|
|
|
func setTargetAttribute(attribute: String, buildSetting: String) {
|
|
if let setting = getSingleBuildSetting(buildSetting) {
|
|
targetAttributes[targetReference, default: [:]].merge([attribute: setting])
|
|
}
|
|
}
|
|
|
|
setTargetAttribute(attribute: "ProvisioningStyle", buildSetting: "CODE_SIGN_STYLE")
|
|
setTargetAttribute(attribute: "DevelopmentTeam", buildSetting: "DEVELOPMENT_TEAM")
|
|
}
|
|
|
|
for target in project.aggregateTargets {
|
|
guard let targetReference = targetAggregateObjects[target.name]?.reference else {
|
|
continue
|
|
}
|
|
generateTargetAttributes(target, targetReference: targetReference)
|
|
}
|
|
|
|
for target in project.targets {
|
|
guard let targetReference = targetObjects[target.name]?.reference else {
|
|
continue
|
|
}
|
|
generateTargetAttributes(target, targetReference: targetReference)
|
|
}
|
|
|
|
return targetAttributes.isEmpty ? nil : ["TargetAttributes": targetAttributes]
|
|
}
|
|
|
|
func sortGroups(group: ObjectReference<PBXGroup>) {
|
|
// sort children
|
|
let children = group.object.children
|
|
.compactMap { reference -> ObjectReference<PBXFileElement>? in
|
|
guard let fileElement = pbxProj.objects.getFileElement(reference: reference) else {
|
|
return nil
|
|
}
|
|
return ObjectReference(reference: reference, object: fileElement)
|
|
}
|
|
.sorted { child1, child2 in
|
|
let sortOrder1 = child1.object.getSortOrder(groupSortPosition: project.options.groupSortPosition)
|
|
let sortOrder2 = child2.object.getSortOrder(groupSortPosition: project.options.groupSortPosition)
|
|
|
|
if sortOrder1 == sortOrder2 {
|
|
return child1.object.nameOrPath.localizedStandardCompare(child2.object.nameOrPath) == .orderedAscending
|
|
} else {
|
|
return sortOrder1 < sortOrder2
|
|
}
|
|
}
|
|
group.object.children = children.map { $0.reference }.filter { $0 != group.reference }
|
|
|
|
// sort sub groups
|
|
let childGroups = group.object.children.compactMap { reference -> ObjectReference<PBXGroup>? in
|
|
guard let group = pbxProj.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(targetType: target.type, sources: target.sources)
|
|
|
|
var plistPath: Path?
|
|
var searchForPlist = true
|
|
var anyDependencyRequiresObjCLinking = false
|
|
|
|
var dependencies: [String] = []
|
|
var targetFrameworkBuildFiles: [String] = []
|
|
var frameworkBuildPaths = Set<String>()
|
|
var copyFilesBuildPhasesFiles: [TargetSource.BuildPhase.CopyFilesSettings: [String]] = [:]
|
|
var copyFrameworksReferences: [String] = []
|
|
var copyResourcesReferences: [String] = []
|
|
var copyWatchReferences: [String] = []
|
|
var extensions: [String] = []
|
|
var carthageFrameworksToEmbed: [String] = []
|
|
|
|
let targetDependencies = (target.transitivelyLinkDependencies ?? project.options.transitivelyLinkDependencies) ?
|
|
getAllDependenciesPlusTransitiveNeedingEmbedding(target: target) : target.dependencies
|
|
|
|
let directlyEmbedCarthage = target.directlyEmbedCarthageDependencies ?? !(target.platform.requiresSimulatorStripping && target.type.isApp)
|
|
|
|
func getEmbedSettings(dependency: Dependency, codeSign: Bool) -> [String: Any] {
|
|
var embedAttributes: [String] = []
|
|
if codeSign {
|
|
embedAttributes.append("CodeSignOnCopy")
|
|
}
|
|
if dependency.removeHeaders {
|
|
embedAttributes.append("RemoveHeadersOnCopy")
|
|
}
|
|
return ["ATTRIBUTES": embedAttributes]
|
|
}
|
|
|
|
for dependency in targetDependencies {
|
|
|
|
let embed = dependency.embed ?? target.shouldEmbedDependencies
|
|
|
|
switch dependency.type {
|
|
case .target:
|
|
let dependencyTargetName = dependency.reference
|
|
let targetDependency = generateTargetDependency(from: target.name, to: dependencyTargetName)
|
|
dependencies.append(targetDependency.reference)
|
|
|
|
guard let dependencyTarget = project.getTarget(dependencyTargetName) else { continue }
|
|
|
|
let dependencyFileReference = targetFileReferences[dependencyTargetName]!
|
|
|
|
let dependecyLinkage = dependencyTarget.defaultLinkage
|
|
let link = dependency.link ?? (
|
|
(dependecyLinkage == .dynamic && target.type != .staticLibrary)
|
|
|| (dependecyLinkage == .static && target.type.isExecutable)
|
|
)
|
|
if link {
|
|
let dependencyBuildFile = targetBuildFiles[dependencyTargetName]!
|
|
let buildFile = createObject(
|
|
id: dependencyBuildFile.reference + target.name,
|
|
PBXBuildFile(fileRef: dependencyBuildFile.object.fileRef!)
|
|
)
|
|
targetFrameworkBuildFiles.append(buildFile.reference)
|
|
|
|
if !anyDependencyRequiresObjCLinking
|
|
&& dependencyTarget.requiresObjCLinking ?? (dependencyTarget.type == .staticLibrary) {
|
|
anyDependencyRequiresObjCLinking = true
|
|
}
|
|
}
|
|
|
|
let embed = dependency.embed ?? (!dependencyTarget.type.isLibrary && (
|
|
target.type.isApp
|
|
|| (target.type.isTest && (dependencyTarget.type.isFramework || dependencyTarget.type == .bundle))
|
|
))
|
|
if embed {
|
|
let embedFile = createObject(
|
|
id: dependencyFileReference + target.name,
|
|
PBXBuildFile(
|
|
fileRef: dependencyFileReference,
|
|
settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? !dependencyTarget.type.isExecutable)
|
|
)
|
|
)
|
|
|
|
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 if dependencyTarget.type == .xpcService {
|
|
copyFilesBuildPhasesFiles[.xpcServices, default: []].append(embedFile.reference)
|
|
} else {
|
|
copyResourcesReferences.append(embedFile.reference)
|
|
}
|
|
}
|
|
|
|
case .framework:
|
|
guard target.type != .staticLibrary else { break }
|
|
|
|
let fileReference: String
|
|
if dependency.implicit {
|
|
fileReference = sourceGenerator.getFileReference(
|
|
path: Path(dependency.reference),
|
|
inPath: project.basePath,
|
|
sourceTree: .buildProductsDir
|
|
)
|
|
} else {
|
|
fileReference = sourceGenerator.getFileReference(
|
|
path: Path(dependency.reference),
|
|
inPath: project.basePath
|
|
)
|
|
}
|
|
|
|
let buildFile = createObject(
|
|
id: "framework" + fileReference + target.name,
|
|
PBXBuildFile(fileRef: fileReference)
|
|
)
|
|
|
|
targetFrameworkBuildFiles.append(buildFile.reference)
|
|
if !frameworkFiles.contains(fileReference) {
|
|
frameworkFiles.append(fileReference)
|
|
}
|
|
|
|
if embed {
|
|
let embedFile = createObject(
|
|
id: "framework embed" + fileReference + target.name,
|
|
PBXBuildFile(fileRef: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true))
|
|
)
|
|
copyFrameworksReferences.append(embedFile.reference)
|
|
}
|
|
|
|
let buildPath = Path(dependency.reference).parent().string.quoted
|
|
frameworkBuildPaths.insert(buildPath)
|
|
|
|
case .carthage:
|
|
guard target.type != .staticLibrary else { break }
|
|
|
|
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: "carthage" + fileReference + target.name,
|
|
PBXBuildFile(fileRef: fileReference)
|
|
)
|
|
|
|
carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference)
|
|
|
|
targetFrameworkBuildFiles.append(buildFile.reference)
|
|
|
|
// Embedding handled by iterating over `carthageDependencies` below
|
|
}
|
|
}
|
|
|
|
for dependency in carthageDependencies {
|
|
guard target.type != .staticLibrary else { break }
|
|
|
|
let embed = dependency.embed ?? target.shouldEmbedDependencies
|
|
|
|
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)
|
|
|
|
if embed {
|
|
if directlyEmbedCarthage {
|
|
let embedFile = createObject(
|
|
id: "carthage embed" + fileReference + target.name,
|
|
PBXBuildFile(fileRef: fileReference, settings: getEmbedSettings(dependency: dependency, codeSign: dependency.codeSign ?? true))
|
|
)
|
|
copyFrameworksReferences.append(embedFile.reference)
|
|
} else {
|
|
carthageFrameworksToEmbed.append(dependency.reference)
|
|
}
|
|
}
|
|
}
|
|
|
|
let fileReference = targetFileReferences[target.name]
|
|
var buildPhases: [String] = []
|
|
|
|
func getBuildFilesForSourceFiles(_ sourceFiles: [SourceFile]) -> [String] {
|
|
let files = sourceFiles
|
|
.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 getBuildFilesForPhase(_ buildPhase: BuildPhase) -> [String] {
|
|
let filteredSourceFiles = sourceFiles
|
|
.filter { $0.buildPhase?.buildPhase == buildPhase }
|
|
return getBuildFilesForSourceFiles(filteredSourceFiles)
|
|
}
|
|
|
|
func getBuildFilesForCopyFilesPhases() -> [TargetSource.BuildPhase.CopyFilesSettings: [String]] {
|
|
var sourceFilesByCopyFiles: [TargetSource.BuildPhase.CopyFilesSettings: [SourceFile]] = [:]
|
|
for sourceFile in sourceFiles {
|
|
guard case let .copyFiles(copyFilesSettings)? = sourceFile.buildPhase else { continue }
|
|
sourceFilesByCopyFiles[copyFilesSettings, default: []].append(sourceFile)
|
|
}
|
|
return sourceFilesByCopyFiles.mapValues { getBuildFilesForSourceFiles($0) }
|
|
}
|
|
|
|
copyFilesBuildPhasesFiles.merge(getBuildFilesForCopyFilesPhases()) { $0 + $1 }
|
|
|
|
buildPhases += try target.prebuildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) }
|
|
|
|
buildPhases += copyFilesBuildPhasesFiles
|
|
.filter { $0.key.phaseOrder == .preCompile }
|
|
.map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) }
|
|
|
|
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)
|
|
}
|
|
|
|
let sourcesBuildPhaseFiles = getBuildFilesForPhase(.sources)
|
|
let sourcesBuildPhase = createObject(id: target.name, PBXSourcesBuildPhase(files: sourcesBuildPhaseFiles))
|
|
buildPhases.append(sourcesBuildPhase.reference)
|
|
|
|
buildPhases += try target.postCompileScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) }
|
|
|
|
let resourcesBuildPhaseFiles = getBuildFilesForPhase(.resources) + copyResourcesReferences
|
|
if !resourcesBuildPhaseFiles.isEmpty {
|
|
let resourcesBuildPhase = createObject(id: target.name, PBXResourcesBuildPhase(files: resourcesBuildPhaseFiles))
|
|
buildPhases.append(resourcesBuildPhase.reference)
|
|
}
|
|
|
|
let buildSettings = project.getCombinedBuildSettings(basePath: project.basePath, target: target, config: project.configs[0])
|
|
let swiftObjCInterfaceHeader = buildSettings["SWIFT_OBJC_INTERFACE_HEADER_NAME"] as? String
|
|
|
|
if target.type == .staticLibrary
|
|
&& swiftObjCInterfaceHeader != ""
|
|
&& sourceFiles.contains(where: { $0.buildPhase == .sources && $0.path.extension == "swift" }) {
|
|
|
|
let inputPaths = ["$(DERIVED_SOURCES_DIR)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"]
|
|
let outputPaths = ["$(BUILT_PRODUCTS_DIR)/include/$(PRODUCT_MODULE_NAME)/$(SWIFT_OBJC_INTERFACE_HEADER_NAME)"]
|
|
let script = createObject(
|
|
id: "Swift.h" + target.name,
|
|
PBXShellScriptBuildPhase(
|
|
files: [],
|
|
name: "Copy Swift Objective-C Interface Header",
|
|
inputPaths: inputPaths,
|
|
outputPaths: outputPaths,
|
|
shellPath: "/bin/sh",
|
|
shellScript: "ditto \"${SCRIPT_INPUT_FILE_0}\" \"${SCRIPT_OUTPUT_FILE_0}\"\n"
|
|
)
|
|
)
|
|
buildPhases.append(script.reference)
|
|
}
|
|
|
|
buildPhases += copyFilesBuildPhasesFiles
|
|
.filter { $0.key.phaseOrder == .postCompile }
|
|
.map { generateCopyFiles(targetName: target.name, copyFiles: $0, buildPhaseFiles: $1) }
|
|
|
|
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,
|
|
name: "Embed App Extensions",
|
|
files: extensions
|
|
)
|
|
)
|
|
|
|
buildPhases.append(copyFilesPhase.reference)
|
|
}
|
|
|
|
copyFrameworksReferences += getBuildFilesForPhase(.frameworks)
|
|
if !copyFrameworksReferences.isEmpty {
|
|
|
|
let copyFilesPhase = createObject(
|
|
id: "embed frameworks" + target.name,
|
|
PBXCopyFilesBuildPhase(
|
|
dstPath: "",
|
|
dstSubfolderSpec: .frameworks,
|
|
name: "Embed 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,
|
|
name: "Embed Watch Content",
|
|
files: copyWatchReferences
|
|
)
|
|
)
|
|
|
|
buildPhases.append(copyFilesPhase.reference)
|
|
}
|
|
|
|
if !carthageFrameworksToEmbed.isEmpty {
|
|
|
|
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 = project.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)
|
|
}
|
|
|
|
let buildRules = target.buildRules.map { buildRule in
|
|
createObject(
|
|
id: "\(target.name)-\(buildRule.action)-\(buildRule.fileType)",
|
|
PBXBuildRule(
|
|
compilerSpec: buildRule.action.compilerSpec,
|
|
fileType: buildRule.fileType.fileType,
|
|
isEditable: true,
|
|
filePatterns: buildRule.fileType.pattern,
|
|
name: buildRule.name ?? "Build Rule",
|
|
outputFiles: buildRule.outputFiles,
|
|
outputFilesCompilerFlags: buildRule.outputFilesCompilerFlags,
|
|
script: buildRule.action.script
|
|
)
|
|
).reference
|
|
}
|
|
|
|
buildPhases += try target.postbuildScripts.map { try generateBuildScript(targetName: target.name, buildScript: $0) }
|
|
|
|
let configs: [ObjectReference<XCBuildConfiguration>] = project.configs.map { config in
|
|
var buildSettings = project.getTargetBuildSettings(target: target, config: config)
|
|
|
|
// automatically set INFOPLIST_FILE path
|
|
if !project.targetHasBuildSetting("INFOPLIST_FILE", basePath: project.basePath, target: target, config: config) {
|
|
if searchForPlist {
|
|
plistPath = getInfoPlist(target.sources)
|
|
searchForPlist = false
|
|
}
|
|
if let plistPath = plistPath {
|
|
buildSettings["INFOPLIST_FILE"] = plistPath.byRemovingBase(path: project.basePath)
|
|
}
|
|
}
|
|
|
|
// automatically calculate bundle id
|
|
if let bundleIdPrefix = project.options.bundleIdPrefix,
|
|
!project.targetHasBuildSetting("PRODUCT_BUNDLE_IDENTIFIER", basePath: project.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,
|
|
!project.targetHasBuildSetting("TEST_TARGET_NAME", basePath: project.basePath, target: target, config: config) {
|
|
for dependency in target.dependencies {
|
|
if dependency.type == .target,
|
|
let dependencyTarget = project.getTarget(dependency.reference),
|
|
dependencyTarget.type == .application {
|
|
buildSettings["TEST_TARGET_NAME"] = dependencyTarget.name
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// objc linkage
|
|
if anyDependencyRequiresObjCLinking {
|
|
let otherLinkingFlags = "OTHER_LDFLAGS"
|
|
let objCLinking = "-ObjC"
|
|
if var array = buildSettings[otherLinkingFlags] as? [String] {
|
|
array.append(objCLinking)
|
|
buildSettings[otherLinkingFlags] = array
|
|
} else if let string = buildSettings[otherLinkingFlags] as? String {
|
|
buildSettings[otherLinkingFlags] = [string, objCLinking]
|
|
} else {
|
|
buildSettings[otherLinkingFlags] = ["$(inherited)", objCLinking]
|
|
}
|
|
}
|
|
|
|
// set Carthage search paths
|
|
let configFrameworkBuildPaths: [String]
|
|
if !carthageDependencies.isEmpty {
|
|
let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + getCarthageBuildPath(platform: target.platform)
|
|
configFrameworkBuildPaths = [carthagePlatformBuildPath] + Array(frameworkBuildPaths).sorted()
|
|
} else {
|
|
configFrameworkBuildPaths = Array(frameworkBuildPaths).sorted()
|
|
}
|
|
|
|
// set framework search paths
|
|
if !configFrameworkBuildPaths.isEmpty {
|
|
let frameworkSearchPaths = "FRAMEWORK_SEARCH_PATHS"
|
|
if var array = buildSettings[frameworkSearchPaths] as? [String] {
|
|
array.append(contentsOf: configFrameworkBuildPaths)
|
|
buildSettings[frameworkSearchPaths] = array
|
|
} else if let string = buildSettings[frameworkSearchPaths] as? String {
|
|
buildSettings[frameworkSearchPaths] = [string] + configFrameworkBuildPaths
|
|
} else {
|
|
buildSettings[frameworkSearchPaths] = ["$(inherited)"] + configFrameworkBuildPaths
|
|
}
|
|
}
|
|
|
|
var baseConfigurationReference: String?
|
|
if let configPath = target.configFiles[config.name] {
|
|
baseConfigurationReference = sourceGenerator.getContainedFileReference(path: project.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: ""
|
|
))
|
|
|
|
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.buildRules = buildRules
|
|
targetObject.productReference = fileReference
|
|
if !target.isLegacy {
|
|
targetObject.productType = target.type
|
|
}
|
|
}
|
|
|
|
func getInfoPlist(_ sources: [TargetSource]) -> Path? {
|
|
return sources
|
|
.lazy
|
|
.map { self.project.basePath + $0.path }
|
|
.compactMap { (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 topLevelTarget: Target) -> [Dependency] {
|
|
// this is used to resolve cyclical target dependencies
|
|
var visitedTargets: Set<String> = []
|
|
var frameworks: [String: Dependency] = [:]
|
|
|
|
var queue: [ProjectTarget] = [topLevelTarget]
|
|
while !queue.isEmpty {
|
|
let projectTarget = queue.removeFirst()
|
|
if visitedTargets.contains(projectTarget.name) {
|
|
continue
|
|
}
|
|
|
|
if let target = projectTarget as? Target {
|
|
for dependency in target.dependencies {
|
|
// don't overwrite frameworks, to allow top level ones to rule
|
|
if frameworks.contains(reference: dependency.reference) {
|
|
continue
|
|
}
|
|
|
|
switch dependency.type {
|
|
case .carthage:
|
|
frameworks[dependency.reference] = dependency
|
|
case .target:
|
|
if let projectTarget = project.getProjectTarget(dependency.reference) {
|
|
queue.append(projectTarget)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
} else if let aggregateTarget = projectTarget as? AggregateTarget {
|
|
for dependencyName in aggregateTarget.targets {
|
|
if let projectTarget = project.getProjectTarget(dependencyName) {
|
|
queue.append(projectTarget)
|
|
}
|
|
}
|
|
}
|
|
|
|
visitedTargets.update(with: projectTarget.name)
|
|
}
|
|
|
|
return frameworks.sorted(by: { $0.key < $1.key }).map { $0.value }
|
|
}
|
|
|
|
func getAllDependenciesPlusTransitiveNeedingEmbedding(target topLevelTarget: Target) -> [Dependency] {
|
|
// this is used to resolve cyclical target dependencies
|
|
var visitedTargets: Set<String> = []
|
|
var dependencies: [String: Dependency] = [:]
|
|
var queue: [Target] = [topLevelTarget]
|
|
while !queue.isEmpty {
|
|
let target = queue.removeFirst()
|
|
if visitedTargets.contains(target.name) {
|
|
continue
|
|
}
|
|
|
|
let isTopLevel = target == topLevelTarget
|
|
|
|
for dependency in target.dependencies {
|
|
// don't overwrite dependencies, to allow top level ones to rule
|
|
if dependencies.contains(reference: dependency.reference) {
|
|
continue
|
|
}
|
|
|
|
// don't want a dependency if it's going to be embedded or statically linked in a non-top level target
|
|
// in .target check we filter out targets that will embed all of their dependencies
|
|
switch dependency.type {
|
|
case .framework, .carthage:
|
|
if isTopLevel || dependency.embed == nil {
|
|
dependencies[dependency.reference] = dependency
|
|
}
|
|
case .target:
|
|
if isTopLevel || dependency.embed == nil {
|
|
if let dependencyTarget = project.getTarget(dependency.reference) {
|
|
dependencies[dependency.reference] = dependency
|
|
if !dependencyTarget.shouldEmbedDependencies {
|
|
// traverse target's dependencies if it doesn't embed them itself
|
|
queue.append(dependencyTarget)
|
|
}
|
|
} else if project.getAggregateTarget(dependency.reference) != nil {
|
|
// Aggregate targets should be included
|
|
dependencies[dependency.reference] = dependency
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
visitedTargets.update(with: target.name)
|
|
}
|
|
|
|
return dependencies.sorted(by: { $0.key < $1.key }).map { $0.value }
|
|
}
|
|
}
|
|
|
|
extension Target {
|
|
|
|
var shouldEmbedDependencies: Bool {
|
|
return type.isApp || type.isTest
|
|
}
|
|
}
|
|
|
|
extension Platform {
|
|
/// - returns: `true` for platforms that the app store requires simulator slices to be stripped.
|
|
public var requiresSimulatorStripping: Bool {
|
|
switch self {
|
|
case .iOS, .tvOS, .watchOS:
|
|
return true
|
|
case .macOS:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PBXFileElement {
|
|
|
|
public func getSortOrder(groupSortPosition: SpecOptions.GroupSortPosition) -> Int {
|
|
if type(of: self).isa == "PBXGroup" {
|
|
switch groupSortPosition {
|
|
case .top: return -1
|
|
case .bottom: return 1
|
|
case .none: return 0
|
|
}
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
}
|