mirror of
https://github.com/yonaskolb/XcodeGen.git
synced 2026-03-18 20:02:25 +00:00
Introduces ability to include Carthage related dependencies when creating a project configuration file
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
//
|
||||
// CarthageDependencyResolver.swift
|
||||
// XcodeGenKit
|
||||
//
|
||||
// Created by Rogerio de Paula Assis on 2/4/19.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ProjectSpec
|
||||
import PathKit
|
||||
|
||||
public struct CarthageDependencyResolver {
|
||||
|
||||
/// Carthage's base build path as specified by the
|
||||
/// project's `SpecOptions`, or `Carthage/Build` by default
|
||||
var baseBuildPath: String {
|
||||
return project.options.carthageBuildPath ?? "Carthage/Build"
|
||||
}
|
||||
|
||||
/// Carthage's executable path as specified by the
|
||||
/// project's `SpecOptions`, or `carthage` by default
|
||||
var executablePath: String {
|
||||
return project.options.carthageExecutablePath ?? "carthage"
|
||||
}
|
||||
|
||||
/// Carthage's build path for the given platform
|
||||
func buildPath(for platform: Platform) -> String {
|
||||
let carthagePath = Path(baseBuildPath)
|
||||
let platformName = platform.carthageDirectoryName
|
||||
return "\(carthagePath)/\(platformName)"
|
||||
}
|
||||
|
||||
// Keeps a cache of previously parsed related dependencies
|
||||
private var carthageCachedRelatedDependencies: [String: CarthageVersionFile] = [:]
|
||||
private let project: Project
|
||||
|
||||
init(project: Project) {
|
||||
self.project = project
|
||||
}
|
||||
|
||||
/// Fetches all carthage dependencies for a given target
|
||||
func dependencies(for 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 {
|
||||
// don't overwrite frameworks, to allow top level ones to rule
|
||||
let nonExistingDepsPredicate: (Dependency) -> Bool = { return frameworks[$0.reference] == nil }
|
||||
let nonExistentDependencies = target.dependencies.filter(nonExistingDepsPredicate)
|
||||
for dependency in nonExistentDependencies {
|
||||
switch dependency.type {
|
||||
case .carthage(let includeRelated):
|
||||
if includeRelated == true {
|
||||
relatedDependencies(for: dependency, in: target.platform)
|
||||
.filter(nonExistingDepsPredicate)
|
||||
.forEach { frameworks[$0.reference] = $0 }
|
||||
} else {
|
||||
frameworks[dependency.reference] = dependency
|
||||
}
|
||||
case .target:
|
||||
if let projectTarget = project.getProjectTarget(dependency.reference) {
|
||||
if let dependencyTarget = projectTarget as? Target {
|
||||
if topLevelTarget.platform == dependencyTarget.platform {
|
||||
queue.append(projectTarget)
|
||||
}
|
||||
} else {
|
||||
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 }
|
||||
}
|
||||
|
||||
/// Reads the .version file generated for a given Carthage dependency
|
||||
/// and returns a list of its related dependencies including self
|
||||
func relatedDependencies(for dependency: Dependency, in platform: Platform) -> [Dependency] {
|
||||
guard
|
||||
case .carthage = dependency.type,
|
||||
let versionFile = try? fetchCarthageVersionFile(for: dependency) else {
|
||||
// No .version file or we've been unable to parse
|
||||
// so fail gracefully by returning the main dependency
|
||||
return [dependency]
|
||||
}
|
||||
return versionFile.references(for: platform)
|
||||
.map { $0.name }
|
||||
.map { Dependency(
|
||||
type: dependency.type,
|
||||
reference: $0,
|
||||
embed: dependency.embed,
|
||||
codeSign: dependency.codeSign,
|
||||
link: dependency.link,
|
||||
implicit: dependency.implicit,
|
||||
weakLink: dependency.weakLink
|
||||
)}
|
||||
.sorted(by: { $0.reference < $1.reference })
|
||||
}
|
||||
|
||||
private func fetchCarthageVersionFile(for dependency: Dependency) throws -> CarthageVersionFile {
|
||||
if let cachedVersionFile = carthageCachedRelatedDependencies[dependency.reference] {
|
||||
return cachedVersionFile
|
||||
}
|
||||
let buildPath = project.basePath + "\(self.baseBuildPath)/.\(dependency.reference).version"
|
||||
let data = try buildPath.read()
|
||||
let carthageVersionFile = try JSONDecoder().decode(CarthageVersionFile.self, from: data)
|
||||
return carthageVersionFile
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodable struct for type safe parsing of the .version file
|
||||
fileprivate struct CarthageVersionFile: Decodable {
|
||||
|
||||
struct Reference: Decodable, Equatable {
|
||||
public let name: String
|
||||
public let hash: String
|
||||
}
|
||||
|
||||
enum Key: String, CodingKey, CaseIterable {
|
||||
case iOS
|
||||
case Mac
|
||||
case tvOS
|
||||
case watchOS
|
||||
}
|
||||
|
||||
private let data: [Key: [Reference]]
|
||||
fileprivate init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: Key.self)
|
||||
data = try Key.allCases.reduce(into: [:]) { (current, nextKey) in
|
||||
let refs = try container.decodeIfPresent([Reference].self, forKey: nextKey)
|
||||
current[nextKey] = refs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CarthageVersionFile {
|
||||
fileprivate func references(for platform: Platform) -> [Reference] {
|
||||
switch platform {
|
||||
case .iOS: return data[.iOS] ?? []
|
||||
case .watchOS: return data[.watchOS] ?? []
|
||||
case .tvOS: return data[.tvOS] ?? []
|
||||
case .macOS: return data[.Mac] ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class PBXProjGenerator {
|
||||
let project: Project
|
||||
|
||||
let pbxProj: PBXProj
|
||||
let carthageResolver: CarthageDependencyResolver
|
||||
|
||||
var sourceGenerator: SourceGenerator!
|
||||
|
||||
var targetObjects: [String: PBXTarget] = [:]
|
||||
@@ -21,12 +23,9 @@ public class PBXProjGenerator {
|
||||
|
||||
var generated = false
|
||||
|
||||
var carthageBuildPath: String {
|
||||
return project.options.carthageBuildPath ?? "Carthage/Build"
|
||||
}
|
||||
|
||||
public init(project: Project) {
|
||||
self.project = project
|
||||
carthageResolver = CarthageDependencyResolver(project: project)
|
||||
pbxProj = PBXProj(rootObject: nil, objectVersion: project.objectVersion)
|
||||
sourceGenerator = SourceGenerator(project: project, pbxProj: pbxProj)
|
||||
}
|
||||
@@ -179,7 +178,7 @@ public class PBXProjGenerator {
|
||||
children: platforms,
|
||||
sourceTree: .group,
|
||||
name: "Carthage",
|
||||
path: carthageBuildPath
|
||||
path: carthageResolver.baseBuildPath
|
||||
)
|
||||
)
|
||||
frameworkFiles.append(carthageGroup)
|
||||
@@ -399,7 +398,7 @@ public class PBXProjGenerator {
|
||||
}
|
||||
|
||||
func generateTarget(_ target: Target) throws {
|
||||
let carthageDependencies = getAllCarthageDependencies(target: target)
|
||||
let carthageDependencies = carthageResolver.dependencies(for: target)
|
||||
|
||||
let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources)
|
||||
.sorted { $0.path.lastComponent < $1.path.lastComponent }
|
||||
@@ -573,26 +572,29 @@ public class PBXProjGenerator {
|
||||
)
|
||||
targetFrameworkBuildFiles.append(buildFile)
|
||||
|
||||
case .carthage:
|
||||
// Static libraries can't link or embed dynamic frameworks
|
||||
guard target.type != .staticLibrary else { break }
|
||||
case .carthage(let includeRelated):
|
||||
let allDependencies = includeRelated
|
||||
? carthageResolver.relatedDependencies(for: dependency, in: target.platform) : [dependency]
|
||||
allDependencies.forEach { dependency in
|
||||
// Static libraries can't link or embed dynamic frameworks
|
||||
guard target.type != .staticLibrary else { return }
|
||||
|
||||
var platformPath = Path(getCarthageBuildPath(platform: target.platform))
|
||||
var frameworkPath = platformPath + dependency.reference
|
||||
if frameworkPath.extension == nil {
|
||||
frameworkPath = Path(frameworkPath.string + ".framework")
|
||||
var platformPath = Path(carthageResolver.buildPath(for: target.platform))
|
||||
var frameworkPath = platformPath + dependency.reference
|
||||
if frameworkPath.extension == nil {
|
||||
frameworkPath = Path(frameworkPath.string + ".framework")
|
||||
}
|
||||
let fileReference = self.sourceGenerator.getFileReference(path: frameworkPath, inPath: platformPath)
|
||||
|
||||
self.carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference)
|
||||
|
||||
if dependency.link ?? true {
|
||||
let buildFile = self.addObject(
|
||||
PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency))
|
||||
)
|
||||
targetFrameworkBuildFiles.append(buildFile)
|
||||
}
|
||||
}
|
||||
let fileReference = sourceGenerator.getFileReference(path: frameworkPath, inPath: platformPath)
|
||||
|
||||
carthageFrameworksByPlatform[target.platform.carthageDirectoryName, default: []].insert(fileReference)
|
||||
|
||||
if dependency.link ?? true {
|
||||
let buildFile = addObject(
|
||||
PBXBuildFile(file: fileReference, settings: getDependencyFrameworkSettings(dependency: dependency))
|
||||
)
|
||||
targetFrameworkBuildFiles.append(buildFile)
|
||||
}
|
||||
|
||||
// Embedding handled by iterating over `carthageDependencies` below
|
||||
}
|
||||
}
|
||||
@@ -602,7 +604,7 @@ public class PBXProjGenerator {
|
||||
|
||||
let embed = dependency.embed ?? target.shouldEmbedDependencies
|
||||
|
||||
var platformPath = Path(getCarthageBuildPath(platform: target.platform))
|
||||
var platformPath = Path(carthageResolver.buildPath(for: target.platform))
|
||||
var frameworkPath = platformPath + dependency.reference
|
||||
if frameworkPath.extension == nil {
|
||||
frameworkPath = Path(frameworkPath.string + ".framework")
|
||||
@@ -710,10 +712,10 @@ public class PBXProjGenerator {
|
||||
if !carthageFrameworksToEmbed.isEmpty {
|
||||
|
||||
let inputPaths = carthageFrameworksToEmbed
|
||||
.map { "$(SRCROOT)/\(carthageBuildPath)/\(target.platform)/\($0)\($0.contains(".") ? "" : ".framework")" }
|
||||
.map { "$(SRCROOT)/\(carthageResolver.baseBuildPath)/\(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 carthageExecutable = carthageResolver.executablePath
|
||||
let carthageScript = addObject(
|
||||
PBXShellScriptBuildPhase(
|
||||
name: "Carthage",
|
||||
@@ -869,7 +871,7 @@ public class PBXProjGenerator {
|
||||
// set Carthage search paths
|
||||
let configFrameworkBuildPaths: [String]
|
||||
if !carthageDependencies.isEmpty {
|
||||
let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + getCarthageBuildPath(platform: target.platform)
|
||||
let carthagePlatformBuildPath = "$(PROJECT_DIR)/" + carthageResolver.buildPath(for: target.platform)
|
||||
configFrameworkBuildPaths = [carthagePlatformBuildPath] + Array(frameworkBuildPaths).sorted()
|
||||
} else {
|
||||
configFrameworkBuildPaths = Array(frameworkBuildPaths).sorted()
|
||||
@@ -936,63 +938,6 @@ public class PBXProjGenerator {
|
||||
.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[dependency.reference] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch dependency.type {
|
||||
case .carthage:
|
||||
frameworks[dependency.reference] = dependency
|
||||
case .target:
|
||||
if let projectTarget = project.getProjectTarget(dependency.reference) {
|
||||
if let dependencyTarget = projectTarget as? Target {
|
||||
if topLevelTarget.platform == dependencyTarget.platform {
|
||||
queue.append(projectTarget)
|
||||
}
|
||||
} else {
|
||||
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> = []
|
||||
|
||||
Reference in New Issue
Block a user