Introduces ability to include Carthage related dependencies when creating a project configuration file

This commit is contained in:
Rogerio de Paula Assis
2019-02-22 21:29:56 -05:00
parent 2710a72e7f
commit 18f52ab098
14 changed files with 518 additions and 106 deletions
@@ -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] ?? []
}
}
}
+30 -85
View File
@@ -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> = []