Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability to include related frameworks when using Carthage #506

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## Master

#### Added

- Added `missingConfigFiles` to `options.disabledValidations` to optionally skip checking for the existence of config files.
- Added ability to automatically include Carthage related dependencies via `includeRelated: true` [#506](https://github.com/yonaskolb/XcodeGen/pull/506) @rpassis

## 2.2.0

Expand Down
5 changes: 5 additions & 0 deletions Docs/ProjectSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Note that target names can also be changed by adding a `name` property to a targ
- `bottom` (default) - at the bottom, after other files
- [ ] **transitivelyLinkDependencies**: **Bool** - If this is `true` then targets will link to the dependencies of their target dependencies. If a target should embed its dependencies, such as application and test bundles, it will embed these transitive dependencies as well. Some complex setups might want to set this to `false` and explicitly specify dependencies at every level. Targets can override this with [Target](#target).transitivelyLinkDependencies. Defaults to `false`.
- [ ] **generateEmptyDirectories**: **Bool** - If this is `true` then empty directories will be added to project too else will be missed. Defaults to `false`.
- [ ] **includeCarthageRelatedDependencies**: **Bool** - When this is set to `true`, any carthage dependency with related dependencies will be included automatically. This flag can be overriden individually for each carthage dependency - for more details see See **includeRelated** in the [Dependency](#dependency) section. Defaults to `false`.

```yaml
options:
Expand Down Expand Up @@ -377,6 +378,9 @@ Carthage frameworks are expected to be in `CARTHAGE_BUILD_PATH/PLATFORM/FRAMEWOR
- `PLATFORM` = the target's platform
- `FRAMEWORK` = the specified name.

If any of the Carthage dependencies has related dependencies, they can be automatically included using the `includeRelated: true` flag.
Xcodegen uses `.version` files generated by Carthage so in order for this to work the dependencies will need to be built / available in the specified Carthage build folder.

If any applications contain carthage dependencies within itself or any dependent targets, a carthage copy files script is automatically added to the application containing all the relevant frameworks. A `FRAMEWORK_SEARCH_PATHS` setting is also automatically added

```yaml
Expand All @@ -386,6 +390,7 @@ targets:
- target: MyFramework
- framework: path/to/framework.framework
- carthage: Result
includeRelated: true
- sdk: Contacts.framework
- sdk: libc++.tbd
MyFramework:
Expand Down
13 changes: 10 additions & 3 deletions Sources/ProjectSpec/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@ public struct Dependency: Equatable {
self.weakLink = weakLink
}

public enum DependencyType {
public enum DependencyType: Equatable {
case target
case framework
case carthage
case carthage(includeRelated: Bool?)
case sdk
}
}

extension Dependency: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(reference)
}
}

extension Dependency: JSONObjectConvertible {

public init(jsonDictionary: JSONDictionary) throws {
Expand All @@ -48,7 +54,8 @@ extension Dependency: JSONObjectConvertible {
type = .framework
reference = framework
} else if let carthage: String = jsonDictionary.json(atKeyPath: "carthage") {
type = .carthage
let includeRelated: Bool? = jsonDictionary.json(atKeyPath: "includeRelated")
type = .carthage(includeRelated: includeRelated)
reference = carthage
} else if let sdk: String = jsonDictionary.json(atKeyPath: "sdk") {
type = .sdk
Expand Down
6 changes: 5 additions & 1 deletion Sources/ProjectSpec/SpecOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public struct SpecOptions: Equatable {
public var transitivelyLinkDependencies: Bool
public var groupSortPosition: GroupSortPosition
public var generateEmptyDirectories: Bool
public var includeCarthageRelatedDependencies: Bool

public enum ValidationType: String {
case missingConfigs
Expand Down Expand Up @@ -74,7 +75,8 @@ public struct SpecOptions: Equatable {
defaultConfig: String? = nil,
transitivelyLinkDependencies: Bool = false,
groupSortPosition: GroupSortPosition = .bottom,
generateEmptyDirectories: Bool = false
generateEmptyDirectories: Bool = false,
includeCarthageRelatedDependencies: Bool = false
) {
self.minimumXcodeGenVersion = minimumXcodeGenVersion
self.carthageBuildPath = carthageBuildPath
Expand All @@ -93,6 +95,7 @@ public struct SpecOptions: Equatable {
self.transitivelyLinkDependencies = transitivelyLinkDependencies
self.groupSortPosition = groupSortPosition
self.generateEmptyDirectories = generateEmptyDirectories
self.includeCarthageRelatedDependencies = includeCarthageRelatedDependencies
}
}

Expand All @@ -119,6 +122,7 @@ extension SpecOptions: JSONObjectConvertible {
transitivelyLinkDependencies = jsonDictionary.json(atKeyPath: "transitivelyLinkDependencies") ?? false
groupSortPosition = jsonDictionary.json(atKeyPath: "groupSortPosition") ?? .bottom
generateEmptyDirectories = jsonDictionary.json(atKeyPath: "generateEmptyDirectories") ?? false
includeCarthageRelatedDependencies = jsonDictionary.json(atKeyPath: "includeCarthageRelatedDependencies") ?? false
}
}

Expand Down
166 changes: 166 additions & 0 deletions Sources/XcodeGenKit/CarthageDependencyResolver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//
// 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: Set<Dependency> = []

var queue: [ProjectTarget] = [topLevelTarget]
rpassis marked this conversation as resolved.
Show resolved Hide resolved
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 nonExistentDependencies = target.dependencies.filter { !frameworks.contains($0) }
for dependency in nonExistentDependencies {
switch dependency.type {
case .carthage(let includeRelated):
let includeRelated = includeRelated ?? project.options.includeCarthageRelatedDependencies
if includeRelated {
relatedDependencies(for: dependency, in: target.platform)
.filter { !frameworks.contains($0) }
.forEach { frameworks.insert($0) }
} else {
frameworks.insert(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.reference < $1.reference })
}

/// 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 { Dependency(
type: dependency.type,
reference: $0.name,
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] ?? []
}
}
}


Loading