Add support for CocoaPods dependencies (#465)
* Add TargetDependency.cocoapods * Add Dependency.cocoapods * Add CocoaPodsNode model and support for caching it to the GraphLoaderCache * Add tests to CocoaPodsNode * Support loading a CocoaPods target dependency * Test TargetNode changes * Lint that the directories referenced by CocoaPods dependencies contain a Podfile * Run 'pod install' after the workspace generation * Add documentation * Update Pod specs repository automtically * Update CHANGELOG * Add acceptance tests * Remove empty code block and fix acceptance test
This commit is contained in:
parent
a3684c66d6
commit
22854327d5
|
@ -15,6 +15,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
- Support multiple header paths https://github.com/tuist/tuist/pull/459 by @adamkhazi
|
||||
- Allow specifying multiple configurations within project manifests https://github.com/tuist/tuist/pull/451 by @kwridan
|
||||
- Add linting for mismatching build configurations in a workspace https://github.com/tuist/tuist/pull/474 by @kwridan
|
||||
- Support for CocoaPods dependencies https://github.com/tuist/tuist/pull/465 by @pepibumur
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -15,3 +15,4 @@ gem "danger-swiftlint", "~> 0.23.0"
|
|||
gem "encrypted-environment", "~> 0.2.0"
|
||||
gem "google-cloud-storage", "~> 1.19"
|
||||
gem "colorize", "~> 0.8.1"
|
||||
gem "cocoapods", "~> 1.7"
|
||||
|
|
62
Gemfile.lock
62
Gemfile.lock
|
@ -2,19 +2,59 @@ GEM
|
|||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.0)
|
||||
activesupport (4.2.11.1)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
ast (2.4.0)
|
||||
atomos (0.1.3)
|
||||
backports (3.11.4)
|
||||
builder (3.2.3)
|
||||
byebug (10.0.2)
|
||||
claide (1.0.2)
|
||||
claide (1.0.3)
|
||||
claide-plugins (0.9.2)
|
||||
cork
|
||||
nap
|
||||
open4 (~> 1.3)
|
||||
cocoapods (1.7.5)
|
||||
activesupport (>= 4.0.2, < 5)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.7.5)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.2.2, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-stats (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.3.1, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.6.6)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (~> 1.4)
|
||||
xcodeproj (>= 1.10.0, < 2.0)
|
||||
cocoapods-core (1.7.5)
|
||||
activesupport (>= 4.0.2, < 6)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.4)
|
||||
cocoapods-downloader (1.2.2)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.0)
|
||||
cocoapods-stats (1.1.0)
|
||||
cocoapods-trunk (1.3.1)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.1.0)
|
||||
colored2 (3.1.2)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.1.5)
|
||||
cork (0.3.0)
|
||||
colored2 (~> 3.1)
|
||||
cucumber (3.1.2)
|
||||
|
@ -64,10 +104,14 @@ GEM
|
|||
ejson (1.2.1)
|
||||
encrypted-environment (0.2.0)
|
||||
ejson (~> 1.2)
|
||||
escape (0.0.4)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-http-cache (2.0.0)
|
||||
faraday (~> 0.8)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
gherkin (5.1.0)
|
||||
git (1.5.0)
|
||||
google-api-client (0.30.5)
|
||||
|
@ -97,6 +141,8 @@ GEM
|
|||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.7)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jaro_winkler (1.5.3)
|
||||
jwt (2.2.1)
|
||||
kramdown (2.1.0)
|
||||
|
@ -108,11 +154,14 @@ GEM
|
|||
mime-types-data (3.2019.0331)
|
||||
mini_mime (1.0.2)
|
||||
minitest (5.11.3)
|
||||
molinillo (0.6.6)
|
||||
multi_json (1.13.1)
|
||||
multi_test (0.1.2)
|
||||
multipart-post (2.1.1)
|
||||
nanaimo (0.2.6)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.0)
|
||||
netrc (0.11.0)
|
||||
no_proxy_fix (0.1.2)
|
||||
octokit (4.14.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
|
@ -136,6 +185,7 @@ GEM
|
|||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
ruby-macho (1.4.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
sawyer (0.8.2)
|
||||
addressable (>= 2.3.5)
|
||||
|
@ -151,14 +201,24 @@ GEM
|
|||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (1.6.0)
|
||||
xcodeproj (1.12.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.2.6)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
byebug (~> 10.0)
|
||||
cocoapods (~> 1.7)
|
||||
colorize (~> 0.8.1)
|
||||
cucumber (~> 3.1)
|
||||
danger (~> 6.0)
|
||||
|
|
|
@ -48,6 +48,12 @@ public enum TargetDependency: Codable, Equatable {
|
|||
/// - status: The dependency status (optional dependencies are weakly linked)
|
||||
case sdk(name: String, status: SDKStatus)
|
||||
|
||||
/// Dependency on CocoaPods pods.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: Path to the directory that contains the Podfile.
|
||||
case cocoapods(path: String)
|
||||
|
||||
/// Dependency on system library or framework
|
||||
///
|
||||
/// - Parameters:
|
||||
|
@ -71,6 +77,8 @@ public enum TargetDependency: Codable, Equatable {
|
|||
return "library"
|
||||
case .sdk:
|
||||
return "sdk"
|
||||
case .cocoapods:
|
||||
return "cocoapods"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +133,9 @@ extension TargetDependency {
|
|||
self = .sdk(name: try container.decode(String.self, forKey: .name),
|
||||
status: try container.decode(SDKStatus.self, forKey: .status))
|
||||
|
||||
case "cocoapods":
|
||||
self = .cocoapods(path: try container.decode(String.self, forKey: .path))
|
||||
|
||||
default:
|
||||
throw CodingError.unknownType(type)
|
||||
}
|
||||
|
@ -150,6 +161,8 @@ extension TargetDependency {
|
|||
case let .sdk(name, status):
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(status, forKey: .status)
|
||||
case let .cocoapods(path):
|
||||
try container.encode(path, forKey: .path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,14 +38,14 @@ public class Printer: Printing {
|
|||
|
||||
public func print(error: Error) {
|
||||
let writer = InteractiveWriter.stderr
|
||||
writer.write("❌ Error: ", inColor: .red, bold: true)
|
||||
writer.write("Error: ", inColor: .red, bold: true)
|
||||
writer.write(error.localizedDescription)
|
||||
writer.write("\n")
|
||||
}
|
||||
|
||||
public func print(success: String) {
|
||||
let writer = InteractiveWriter.stdout
|
||||
writer.write("✅ Success: ", inColor: .green, bold: true)
|
||||
writer.write("Success: ", inColor: .green, bold: true)
|
||||
writer.write(success)
|
||||
writer.write("\n")
|
||||
}
|
||||
|
@ -62,14 +62,14 @@ public class Printer: Printing {
|
|||
|
||||
public func print(warning: String) {
|
||||
let writer = InteractiveWriter.stdout
|
||||
writer.write("⚠️ Warning: ", inColor: .yellow, bold: true)
|
||||
writer.write("Warning: ", inColor: .yellow, bold: true)
|
||||
writer.write(warning, inColor: .yellow, bold: false)
|
||||
writer.write("\n")
|
||||
}
|
||||
|
||||
public func print(errorMessage: String) {
|
||||
let writer = InteractiveWriter.stderr
|
||||
writer.write("❌ Error: ", inColor: .red, bold: true)
|
||||
writer.write("Error: ", inColor: .red, bold: true)
|
||||
writer.write(errorMessage, inColor: .red, bold: false)
|
||||
writer.write("\n")
|
||||
}
|
||||
|
|
|
@ -85,6 +85,16 @@ public protocol Systeming {
|
|||
/// - Throws: An error if the command fails.
|
||||
func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String]) throws
|
||||
|
||||
/// Runs a command in the shell printing its output.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - arguments: Command.
|
||||
/// - verbose: When true it prints the command that will be executed before executing it.
|
||||
/// - environment: Environment that should be used when running the task.
|
||||
/// - redirection: Instance through which the output will be redirected.
|
||||
/// - Throws: An error if the command fails.
|
||||
func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String], redirection: Basic.Process.OutputRedirection) throws
|
||||
|
||||
/// Runs a command in the shell asynchronously.
|
||||
/// When the process that triggers the command gets killed, the command continues its execution.
|
||||
///
|
||||
|
@ -283,7 +293,6 @@ public final class System: Systeming {
|
|||
/// - Parameters:
|
||||
/// - arguments: Arguments to be passed.
|
||||
/// - verbose: When true it prints the command that will be executed before executing it.
|
||||
/// - workingDirectoryPath: The working directory path the task is executed from.
|
||||
/// - environment: Environment that should be used when running the task.
|
||||
/// - Throws: An error if the command fails.
|
||||
public func runAndPrint(_ arguments: String...,
|
||||
|
@ -304,16 +313,34 @@ public final class System: Systeming {
|
|||
public func runAndPrint(_ arguments: [String],
|
||||
verbose: Bool,
|
||||
environment: [String: String]) throws {
|
||||
try runAndPrint(arguments,
|
||||
verbose: verbose,
|
||||
environment: environment,
|
||||
redirection: .none)
|
||||
}
|
||||
|
||||
/// Runs a command in the shell printing its output.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - arguments: Command.
|
||||
/// - verbose: When true it prints the command that will be executed before executing it.
|
||||
/// - environment: Environment that should be used when running the task.
|
||||
/// - redirection: Instance through which the output will be redirected.
|
||||
/// - Throws: An error if the command fails.
|
||||
public func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String], redirection: Basic.Process.OutputRedirection) throws {
|
||||
let process = Process(arguments: arguments,
|
||||
environment: environment,
|
||||
outputRedirection: .stream(stdout: { bytes in
|
||||
FileHandle.standardOutput.write(Data(bytes))
|
||||
redirection.outputClosures?.stdoutClosure(bytes)
|
||||
}, stderr: { bytes in
|
||||
FileHandle.standardError.write(Data(bytes))
|
||||
redirection.outputClosures?.stderrClosure(bytes)
|
||||
}), verbose: verbose,
|
||||
startNewProcessGroup: false)
|
||||
try process.launch()
|
||||
try process.waitUntilExit()
|
||||
let result = try process.waitUntilExit()
|
||||
try result.throwIfErrored()
|
||||
}
|
||||
|
||||
/// Runs a command in the shell asynchronously.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import struct Basic.AbsolutePath
|
||||
import Basic
|
||||
import Foundation
|
||||
import TuistCore
|
||||
|
||||
|
@ -13,7 +13,7 @@ public final class MockSystem: Systeming {
|
|||
public init() {}
|
||||
|
||||
public func errorCommand(_ arguments: String..., error: String? = nil) {
|
||||
self.errorCommand(arguments, error: error)
|
||||
errorCommand(arguments, error: error)
|
||||
}
|
||||
|
||||
public func errorCommand(_ arguments: [String], error: String? = nil) {
|
||||
|
@ -21,7 +21,7 @@ public final class MockSystem: Systeming {
|
|||
}
|
||||
|
||||
public func succeedCommand(_ arguments: String..., output: String? = nil) {
|
||||
self.succeedCommand(arguments, output: output)
|
||||
succeedCommand(arguments, output: output)
|
||||
}
|
||||
|
||||
public func succeedCommand(_ arguments: [String], output: String? = nil) {
|
||||
|
@ -39,19 +39,19 @@ public final class MockSystem: Systeming {
|
|||
}
|
||||
|
||||
public func run(_ arguments: String...) throws {
|
||||
try self.run(arguments)
|
||||
try run(arguments)
|
||||
}
|
||||
|
||||
public func capture(_ arguments: [String]) throws -> String {
|
||||
return try self.capture(arguments, verbose: false, environment: [:])
|
||||
return try capture(arguments, verbose: false, environment: [:])
|
||||
}
|
||||
|
||||
public func capture(_ arguments: String...) throws -> String {
|
||||
return try self.capture(arguments, verbose: false, environment: [:])
|
||||
return try capture(arguments, verbose: false, environment: [:])
|
||||
}
|
||||
|
||||
public func capture(_ arguments: String..., verbose: Bool, environment: [String: String]) throws -> String {
|
||||
return try self.capture(arguments, verbose: verbose, environment: environment)
|
||||
return try capture(arguments, verbose: verbose, environment: environment)
|
||||
}
|
||||
|
||||
public func capture(_ arguments: [String], verbose _: Bool, environment _: [String: String]) throws -> String {
|
||||
|
@ -66,29 +66,36 @@ public final class MockSystem: Systeming {
|
|||
}
|
||||
|
||||
public func runAndPrint(_ arguments: String...) throws {
|
||||
try self.runAndPrint(arguments)
|
||||
try runAndPrint(arguments)
|
||||
}
|
||||
|
||||
public func runAndPrint(_ arguments: [String]) throws {
|
||||
try self.runAndPrint(arguments, verbose: false, environment: [:])
|
||||
try runAndPrint(arguments, verbose: false, environment: [:])
|
||||
}
|
||||
|
||||
public func runAndPrint(_ arguments: String..., verbose: Bool, environment: [String: String]) throws {
|
||||
try self.runAndPrint(arguments, verbose: verbose, environment: environment)
|
||||
try runAndPrint(arguments, verbose: verbose, environment: environment)
|
||||
}
|
||||
|
||||
public func runAndPrint(_ arguments: [String], verbose _: Bool, environment _: [String: String]) throws {
|
||||
public func runAndPrint(_ arguments: [String], verbose: Bool, environment: [String: String]) throws {
|
||||
try runAndPrint(arguments, verbose: verbose, environment: environment, redirection: .none)
|
||||
}
|
||||
|
||||
public func runAndPrint(_ arguments: [String], verbose _: Bool, environment _: [String: String], redirection: Basic.Process.OutputRedirection) throws {
|
||||
let command = arguments.joined(separator: " ")
|
||||
guard let stub = self.stubs[command] else {
|
||||
throw SystemError.terminated(code: 1, error: "command '\(command)' not stubbed")
|
||||
}
|
||||
if stub.exitstatus != 0 {
|
||||
if let error = stub.stderror {
|
||||
redirection.outputClosures?.stderrClosure([UInt8](error.data(using: .utf8)!))
|
||||
}
|
||||
throw SystemError.terminated(code: 1, error: stub.stderror ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
public func async(_ arguments: [String]) throws {
|
||||
try self.async(arguments, verbose: false, environment: [:])
|
||||
try async(arguments, verbose: false, environment: [:])
|
||||
}
|
||||
|
||||
public func async(_ arguments: [String], verbose _: Bool, environment _: [String: String]) throws {
|
||||
|
|
|
@ -66,11 +66,13 @@ public class Generator: Generating {
|
|||
system: system,
|
||||
fileHandler: fileHandler)
|
||||
let workspaceStructureGenerator = WorkspaceStructureGenerator(fileHandler: fileHandler)
|
||||
let cocoapodsInteractor = CocoaPodsInteractor()
|
||||
let workspaceGenerator = WorkspaceGenerator(system: system,
|
||||
printer: printer,
|
||||
projectGenerator: projectGenerator,
|
||||
fileHandler: fileHandler,
|
||||
workspaceStructureGenerator: workspaceStructureGenerator)
|
||||
workspaceStructureGenerator: workspaceStructureGenerator,
|
||||
cocoapodsInteractor: cocoapodsInteractor)
|
||||
self.init(graphLoader: graphLoader,
|
||||
workspaceGenerator: workspaceGenerator,
|
||||
projectGenerator: projectGenerator)
|
||||
|
|
|
@ -45,13 +45,15 @@ final class WorkspaceGenerator: WorkspaceGenerating {
|
|||
private let printer: Printing
|
||||
private let fileHandler: FileHandling
|
||||
private let workspaceStructureGenerator: WorkspaceStructureGenerating
|
||||
private let cocoapodsInteractor: CocoaPodsInteracting
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
convenience init(system: Systeming = System(),
|
||||
printer: Printing = Printer(),
|
||||
fileHandler: FileHandling = FileHandler(),
|
||||
defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider()) {
|
||||
defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider(),
|
||||
cocoapodsInteractor: CocoaPodsInteracting = CocoaPodsInteractor()) {
|
||||
let configGenerator = ConfigGenerator(defaultSettingsProvider: defaultSettingsProvider)
|
||||
let targetGenerator = TargetGenerator(configGenerator: configGenerator)
|
||||
let projectGenerator = ProjectGenerator(targetGenerator: targetGenerator,
|
||||
|
@ -63,19 +65,22 @@ final class WorkspaceGenerator: WorkspaceGenerating {
|
|||
printer: printer,
|
||||
projectGenerator: projectGenerator,
|
||||
fileHandler: fileHandler,
|
||||
workspaceStructureGenerator: WorkspaceStructureGenerator(fileHandler: fileHandler))
|
||||
workspaceStructureGenerator: WorkspaceStructureGenerator(fileHandler: fileHandler),
|
||||
cocoapodsInteractor: cocoapodsInteractor)
|
||||
}
|
||||
|
||||
init(system: Systeming,
|
||||
printer: Printing,
|
||||
projectGenerator: ProjectGenerating,
|
||||
fileHandler: FileHandling,
|
||||
workspaceStructureGenerator: WorkspaceStructureGenerating) {
|
||||
workspaceStructureGenerator: WorkspaceStructureGenerating,
|
||||
cocoapodsInteractor: CocoaPodsInteracting) {
|
||||
self.system = system
|
||||
self.printer = printer
|
||||
self.projectGenerator = projectGenerator
|
||||
self.fileHandler = fileHandler
|
||||
self.workspaceStructureGenerator = workspaceStructureGenerator
|
||||
self.cocoapodsInteractor = cocoapodsInteractor
|
||||
}
|
||||
|
||||
// MARK: - WorkspaceGenerating
|
||||
|
@ -123,6 +128,10 @@ final class WorkspaceGenerator: WorkspaceGenerating {
|
|||
|
||||
try write(xcworkspace: xcWorkspace, to: workspacePath)
|
||||
|
||||
// CocoaPods
|
||||
|
||||
try cocoapodsInteractor.install(graph: graph)
|
||||
|
||||
return workspacePath
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
class CocoaPodsNode: GraphNode {
|
||||
/// Path to the Podfile.
|
||||
var podfilePath: AbsolutePath {
|
||||
return path.appending(component: "Podfile")
|
||||
}
|
||||
|
||||
/// Initializes the node with the path to the directory
|
||||
/// that contains the Podfile.
|
||||
///
|
||||
/// - Parameter path: Path to the directory that contains the Podfile.
|
||||
init(path: AbsolutePath) {
|
||||
super.init(path: path, name: "CocoaPods")
|
||||
}
|
||||
|
||||
/// Compares the CocoaPods node with another node and returns true if both nodes are equal.
|
||||
///
|
||||
/// - Parameter otherNode: The other node to be compared with.
|
||||
/// - Returns: True if the two instances are equal.
|
||||
override func isEqual(to otherNode: GraphNode) -> Bool {
|
||||
guard let otherTagetNode = otherNode as? CocoaPodsNode else {
|
||||
return false
|
||||
}
|
||||
return super.isEqual(to: otherTagetNode)
|
||||
}
|
||||
|
||||
/// Reads the CocoaPods node. If it it exists in the cache, it returns it from the cache.
|
||||
/// Otherwise, it initializes it, stores it in the cache, and then returns it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: Path to the directory that contains the Podfile.
|
||||
/// - cache: Cache instance where the nodes are cached.
|
||||
/// - Returns: The initialized instance of the CocoaPods node.
|
||||
static func read(path: AbsolutePath,
|
||||
cache: GraphLoaderCaching) -> CocoaPodsNode {
|
||||
if let cached = cache.cocoapods(path) { return cached }
|
||||
let node = CocoaPodsNode(path: path)
|
||||
cache.add(cocoapods: node)
|
||||
return node
|
||||
}
|
||||
}
|
|
@ -75,6 +75,11 @@ protocol Graphing: AnyObject, Encodable {
|
|||
var entryPath: AbsolutePath { get }
|
||||
var entryNodes: [GraphNode] { get }
|
||||
var projects: [Project] { get }
|
||||
|
||||
/// Returns all the CocoaPods nodes that are part of the graph.
|
||||
var cocoapods: [CocoaPodsNode] { get }
|
||||
|
||||
/// Returns all the frameorks that are part of the graph.
|
||||
var frameworks: [FrameworkNode] { get }
|
||||
|
||||
/// Returns all the precompiled nodes that are part of the graph.
|
||||
|
@ -133,6 +138,12 @@ class Graph: Graphing {
|
|||
|
||||
// MARK: - Internal
|
||||
|
||||
/// Returns all the CocoaPods nodes that are part of the graph.
|
||||
var cocoapods: [CocoaPodsNode] {
|
||||
return Array(cache.cocoapodsNodes.values)
|
||||
}
|
||||
|
||||
/// Returns all the frameworks that are part of the graph
|
||||
var frameworks: [FrameworkNode] {
|
||||
return cache.precompiledNodes.values.compactMap { $0 as? FrameworkNode }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ protocol GraphLoaderCaching: AnyObject {
|
|||
var projects: [AbsolutePath: Project] { get }
|
||||
var targetNodes: [AbsolutePath: [String: TargetNode]] { get }
|
||||
var precompiledNodes: [AbsolutePath: PrecompiledNode] { get }
|
||||
|
||||
func project(_ path: AbsolutePath) -> Project?
|
||||
func add(project: Project)
|
||||
func add(precompiledNode: PrecompiledNode)
|
||||
|
@ -13,6 +14,20 @@ protocol GraphLoaderCaching: AnyObject {
|
|||
func targetNode(_ path: AbsolutePath, name: String) -> TargetNode?
|
||||
func tuistConfig(_ path: AbsolutePath) -> TuistConfig?
|
||||
func add(tuistConfig: TuistConfig, path: AbsolutePath)
|
||||
|
||||
/// Cached CocoaPods nodes
|
||||
var cocoapodsNodes: [AbsolutePath: CocoaPodsNode] { get }
|
||||
|
||||
/// Returns, if it exists, the CocoaPods node at the given path.
|
||||
///
|
||||
/// - Parameter path: Path to the directory where the Podfile is defined.
|
||||
/// - Returns: The CocoaPods node if it exists in the cache.
|
||||
func cocoapods(_ path: AbsolutePath) -> CocoaPodsNode?
|
||||
|
||||
/// Adds a parsed CocoaPods graph node to the cache.
|
||||
///
|
||||
/// - Parameter cocoapods: Node to be added to the cache.
|
||||
func add(cocoapods: CocoaPodsNode)
|
||||
}
|
||||
|
||||
/// Graph loader cache.
|
||||
|
@ -24,6 +39,24 @@ class GraphLoaderCache: GraphLoaderCaching {
|
|||
var precompiledNodes: [AbsolutePath: PrecompiledNode] = [:]
|
||||
var targetNodes: [AbsolutePath: [String: TargetNode]] = [:]
|
||||
|
||||
/// Cached CocoaPods nodes
|
||||
var cocoapodsNodes: [AbsolutePath: CocoaPodsNode] = [:]
|
||||
|
||||
/// Returns, if it exists, the CocoaPods node at the given path.
|
||||
///
|
||||
/// - Parameter path: Path to the directory where the Podfile is defined.
|
||||
/// - Returns: The CocoaPods node if it exists in the cache.
|
||||
func cocoapods(_ path: AbsolutePath) -> CocoaPodsNode? {
|
||||
return cocoapodsNodes[path]
|
||||
}
|
||||
|
||||
/// Adds a parsed CocoaPods graph node to the cache.
|
||||
///
|
||||
/// - Parameter cocoapods: Node to be added to the cache.
|
||||
func add(cocoapods: CocoaPodsNode) {
|
||||
cocoapodsNodes[cocoapods.path] = cocoapods
|
||||
}
|
||||
|
||||
func tuistConfig(_ path: AbsolutePath) -> TuistConfig? {
|
||||
return tuistConfigs[path]
|
||||
}
|
||||
|
|
|
@ -89,6 +89,8 @@ class TargetNode: GraphNode {
|
|||
return targetDependency.target.name
|
||||
} else if let precompiledDependency = dependency as? PrecompiledNode {
|
||||
return precompiledDependency.name
|
||||
} else if let cocoapodsDependency = dependency as? CocoaPodsNode {
|
||||
return cocoapodsDependency.name
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
@ -128,6 +130,8 @@ class TargetNode: GraphNode {
|
|||
fileHandler: fileHandler, cache: cache)
|
||||
case let .sdk(name, status):
|
||||
return try SDKNode(name: name, platform: platform, status: status)
|
||||
case let .cocoapods(podsPath):
|
||||
return CocoaPodsNode.read(path: path.appending(podsPath), cache: cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,10 +55,25 @@ class GraphLinter: GraphLinting {
|
|||
}
|
||||
|
||||
issues.append(contentsOf: lintCarthageDependencies(graph: graph))
|
||||
issues.append(contentsOf: lintCocoaPodsDependencies(graph: graph))
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
/// It verifies that the directory specified by the CocoaPods dependencies contains a Podfile file.
|
||||
///
|
||||
/// - Parameter graph: Project graph.
|
||||
/// - Returns: Linting issues.
|
||||
private func lintCocoaPodsDependencies(graph: Graphing) -> [LintingIssue] {
|
||||
return graph.cocoapods.compactMap { node in
|
||||
let podfilePath = node.podfilePath
|
||||
if !fileHandler.exists(podfilePath) {
|
||||
return LintingIssue(reason: "The Podfile at path \(podfilePath) referenced by some projects does not exist", severity: .error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func lintCarthageDependencies(graph: Graphing) -> [LintingIssue] {
|
||||
let frameworks = graph.frameworks
|
||||
let carthageFrameworks = frameworks.filter { $0.isCarthage }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Foundation
|
||||
import TuistCore
|
||||
|
||||
|
|
|
@ -12,4 +12,5 @@ public enum Dependency: Equatable {
|
|||
case framework(path: RelativePath)
|
||||
case library(path: RelativePath, publicHeaders: RelativePath, swiftModuleMap: RelativePath?)
|
||||
case sdk(name: String, status: SDKStatus)
|
||||
case cocoapods(path: RelativePath)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
import Foundation
|
||||
import TuistCore
|
||||
|
||||
enum CocoaPodsInteractorError: FatalError, Equatable {
|
||||
/// Thrown when CocoaPods cannot be found.
|
||||
case cocoapodsNotFound
|
||||
case outdatedRepository
|
||||
|
||||
/// Error type.
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .cocoapodsNotFound:
|
||||
return .abort
|
||||
case .outdatedRepository:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
|
||||
/// Error description.
|
||||
var description: String {
|
||||
switch self {
|
||||
case .cocoapodsNotFound:
|
||||
return "CocoaPods was not found either in Bundler nor in the environment"
|
||||
case .outdatedRepository:
|
||||
return "The installation of CocoaPods dependencies might have failed because the CocoaPods repository is outdated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol CocoaPodsInteracting {
|
||||
/// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph.
|
||||
///
|
||||
/// - Parameter graph: Project graph.
|
||||
/// - Throws: An error if the installation of the pods fails.
|
||||
func install(graph: Graphing) throws
|
||||
}
|
||||
|
||||
final class CocoaPodsInteractor: CocoaPodsInteracting {
|
||||
/// Instance to output information to the user.
|
||||
let printer: Printing
|
||||
|
||||
/// Instance to run commands in the system.
|
||||
let system: Systeming
|
||||
|
||||
/// Initializes the CocoaPods
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - printer: Instance to output information to the user.
|
||||
/// - system: Instance to run commands in the system.
|
||||
init(printer: Printing = Printer(),
|
||||
system: Systeming = System()) {
|
||||
self.printer = printer
|
||||
self.system = system
|
||||
}
|
||||
|
||||
/// Runs 'pod install' for all the CocoaPods dependencies that have been indicated in the graph.
|
||||
///
|
||||
/// - Parameter graph: Project graph.
|
||||
/// - Throws: An error if the installation of the pods fails.
|
||||
func install(graph: Graphing) throws {
|
||||
do {
|
||||
try install(graph: graph, updatingRepo: false)
|
||||
} catch let error as CocoaPodsInteractorError {
|
||||
if case CocoaPodsInteractorError.outdatedRepository = error {
|
||||
printer.print(warning: "The local CocoaPods specs repository is outdated. Re-running 'pod install' updating the repository.")
|
||||
try self.install(graph: graph, updatingRepo: true)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func install(graph: Graphing, updatingRepo: Bool) throws {
|
||||
guard !graph.cocoapods.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let canUseBundler = canUseCocoaPodsThroughBundler()
|
||||
let canUseSystem = canUseSystemPod()
|
||||
|
||||
try graph.cocoapods.forEach { node in
|
||||
var command: [String]
|
||||
|
||||
if canUseBundler {
|
||||
command = ["bundle", "exec", "pod"]
|
||||
} else if canUseSystem {
|
||||
command = ["pod"]
|
||||
} else {
|
||||
throw CocoaPodsInteractorError.cocoapodsNotFound
|
||||
}
|
||||
|
||||
command.append(contentsOf: ["install", "--project-directory=\(node.path.pathString)"])
|
||||
|
||||
if updatingRepo {
|
||||
command.append("--repo-update")
|
||||
}
|
||||
|
||||
// The installation of Pods might fail if the local repository that contains the specs
|
||||
// is outdated.
|
||||
printer.print(section: "Installing CocoaPods dependencies defined in \(node.podfilePath)")
|
||||
var mightNeedRepoUpdate: Bool = false
|
||||
let outputClosure: ([UInt8]) -> Void = { bytes in
|
||||
let content = String(data: Data(bytes), encoding: .utf8)
|
||||
if content?.contains("CocoaPods could not find compatible versions for pod") == true {
|
||||
mightNeedRepoUpdate = true
|
||||
}
|
||||
}
|
||||
do {
|
||||
try system.runAndPrint(command,
|
||||
verbose: false,
|
||||
environment: system.env,
|
||||
redirection: .stream(stdout: outputClosure,
|
||||
stderr: outputClosure))
|
||||
} catch {
|
||||
if mightNeedRepoUpdate {
|
||||
throw CocoaPodsInteractorError.outdatedRepository
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if CocoaPods is accessible through Bundler,
|
||||
/// and shoudl be used instead of the global CocoaPods.
|
||||
///
|
||||
/// - Returns: True if Bundler can execute CocoaPods.
|
||||
fileprivate func canUseCocoaPodsThroughBundler() -> Bool {
|
||||
do {
|
||||
try system.run(["bundle", "show", "cocoapods"])
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if CocoaPods is avaiable in the environment.
|
||||
///
|
||||
/// - Returns: True if CocoaPods is available globally in the system.
|
||||
fileprivate func canUseSystemPod() -> Bool {
|
||||
do {
|
||||
_ = try system.which("pod")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -481,6 +481,8 @@ extension TuistGenerator.Dependency {
|
|||
case let .sdk(name, status):
|
||||
return .sdk(name: name,
|
||||
status: .from(manifest: status))
|
||||
case let .cocoapods(path):
|
||||
return .cocoapods(path: RelativePath(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import Foundation
|
||||
import ProjectDescription
|
||||
import TuistCore
|
||||
|
|
|
@ -48,4 +48,12 @@ final class TargetDependencyTests: XCTestCase {
|
|||
// Then
|
||||
XCTAssertEqual(decoded, sdks)
|
||||
}
|
||||
|
||||
func test_cocoapods_codable() throws {
|
||||
// Given
|
||||
let subject = TargetDependency.cocoapods(path: "./path")
|
||||
|
||||
// Then
|
||||
XCTAssertCodable(subject)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ final class WorkspaceGeneratorTests: XCTestCase {
|
|||
var subject: WorkspaceGenerator!
|
||||
var path: AbsolutePath!
|
||||
var fileHandler: MockFileHandler!
|
||||
var cocoapodsInteractor: MockCocoaPodsInteractor!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
@ -16,11 +17,13 @@ final class WorkspaceGeneratorTests: XCTestCase {
|
|||
do {
|
||||
fileHandler = try MockFileHandler()
|
||||
path = fileHandler.currentPath
|
||||
cocoapodsInteractor = MockCocoaPodsInteractor()
|
||||
|
||||
subject = WorkspaceGenerator(
|
||||
system: MockSystem(),
|
||||
printer: MockPrinter(),
|
||||
fileHandler: fileHandler
|
||||
fileHandler: fileHandler,
|
||||
cocoapodsInteractor: cocoapodsInteractor
|
||||
)
|
||||
|
||||
} catch {
|
||||
|
@ -107,6 +110,27 @@ final class WorkspaceGeneratorTests: XCTestCase {
|
|||
])
|
||||
}
|
||||
|
||||
func test_generate_runsPodInstall() throws {
|
||||
// Given
|
||||
let target = anyTarget()
|
||||
let project = Project.test(path: path,
|
||||
name: "Test",
|
||||
settings: .default,
|
||||
targets: [target])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [(target, [])])
|
||||
let workspace = Workspace.test(projects: [project.path])
|
||||
|
||||
// When
|
||||
_ = try subject.generate(workspace: workspace,
|
||||
path: path,
|
||||
graph: graph,
|
||||
tuistConfig: .test())
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(cocoapodsInteractor.installArgs.count, 1)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
func anyTarget() -> Target {
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import TuistGenerator
|
||||
|
||||
final class CocoaPodsNodeTests: XCTestCase {
|
||||
func test_name() {
|
||||
// Given
|
||||
let path = AbsolutePath("/")
|
||||
let subject = CocoaPodsNode(path: path)
|
||||
|
||||
// When
|
||||
let got = subject.name
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, "CocoaPods")
|
||||
}
|
||||
|
||||
func test_isEqual_returnsTrue_when_thePathsAreTheSame() {
|
||||
// Given
|
||||
let lhs = CocoaPodsNode(path: AbsolutePath("/"))
|
||||
let rhs = CocoaPodsNode(path: AbsolutePath("/"))
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(lhs, rhs)
|
||||
}
|
||||
|
||||
func test_isEqual_returnsFalse_when_thePathsAreTheSame() {
|
||||
// Given
|
||||
let lhs = CocoaPodsNode(path: AbsolutePath("/"))
|
||||
let rhs = CocoaPodsNode(path: AbsolutePath("/other"))
|
||||
|
||||
// Then
|
||||
XCTAssertNotEqual(lhs, rhs)
|
||||
}
|
||||
}
|
|
@ -18,6 +18,17 @@ final class MockGraphLoaderCache: GraphLoaderCaching {
|
|||
var targetNodeStub: ((AbsolutePath, String) -> TargetNode?)?
|
||||
var tuistConfigStub: [AbsolutePath: TuistConfig] = [:]
|
||||
var addTuistConfigArgs: [(tuistConfig: TuistConfig, path: AbsolutePath)] = []
|
||||
var cocoapodsNodes: [AbsolutePath: CocoaPodsNode] = [:]
|
||||
var cocoapodsStub: [AbsolutePath: CocoaPodsNode] = [:]
|
||||
var addCococaPodsArgs: [CocoaPodsNode] = []
|
||||
|
||||
func cocoapods(_ path: AbsolutePath) -> CocoaPodsNode? {
|
||||
return cocoapodsStub[path]
|
||||
}
|
||||
|
||||
func add(cocoapods: CocoaPodsNode) {
|
||||
addCococaPodsArgs.append(cocoapods)
|
||||
}
|
||||
|
||||
func tuistConfig(_ path: AbsolutePath) -> TuistConfig? {
|
||||
return tuistConfigStub[path]
|
||||
|
|
|
@ -53,9 +53,10 @@ final class TargetNodeTests: XCTestCase {
|
|||
// Given
|
||||
let library = LibraryNode.test()
|
||||
let framework = FrameworkNode.test()
|
||||
let cocoapods = CocoaPodsNode.test()
|
||||
let node = TargetNode(project: .test(path: "/"),
|
||||
target: .test(name: "Target"),
|
||||
dependencies: [library, framework])
|
||||
dependencies: [library, framework, cocoapods])
|
||||
|
||||
let expected = """
|
||||
{
|
||||
|
@ -66,7 +67,8 @@ final class TargetNodeTests: XCTestCase {
|
|||
"name" : "\(node.target.name)",
|
||||
"dependencies" : [
|
||||
"\(library.name)",
|
||||
"\(framework.name)"
|
||||
"\(framework.name)",
|
||||
"\(cocoapods.name)"
|
||||
],
|
||||
"platform" : "\(node.target.platform.rawValue)"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
@testable import TuistGenerator
|
||||
|
||||
extension CocoaPodsNode {
|
||||
static func test(path: AbsolutePath = "/") -> CocoaPodsNode {
|
||||
return CocoaPodsNode(path: path)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
@testable import TuistGenerator
|
||||
|
||||
extension FrameworkNode {
|
||||
static func test(path: AbsolutePath = "/Test.framework") -> FrameworkNode {
|
||||
return FrameworkNode(path: path)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
@testable import TuistGenerator
|
||||
|
||||
extension LibraryNode {
|
||||
static func test(path: AbsolutePath = "/libTest.a",
|
||||
publicHeaders: AbsolutePath = "/TestHeaders/") -> LibraryNode {
|
||||
return LibraryNode(path: path, publicHeaders: publicHeaders)
|
||||
}
|
||||
}
|
|
@ -12,16 +12,3 @@ extension TargetNode {
|
|||
dependencies: dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
extension FrameworkNode {
|
||||
static func test(path: AbsolutePath = "/Test.framework") -> FrameworkNode {
|
||||
return FrameworkNode(path: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension LibraryNode {
|
||||
static func test(path: AbsolutePath = "/libTest.a",
|
||||
publicHeaders: AbsolutePath = "/TestHeaders/") -> LibraryNode {
|
||||
return LibraryNode(path: path, publicHeaders: publicHeaders)
|
||||
}
|
||||
}
|
|
@ -35,6 +35,21 @@ final class GraphLinterTests: XCTestCase {
|
|||
XCTAssertTrue(result.contains(LintingIssue(reason: "Framework not found at path \(frameworkBPath.pathString). The path might be wrong or Carthage dependencies not fetched", severity: .warning)))
|
||||
}
|
||||
|
||||
func test_lint_when_podfiles_are_missing() throws {
|
||||
// Given
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
let cocoapods = CocoaPodsNode(path: fileHandler.currentPath)
|
||||
cache.add(cocoapods: cocoapods)
|
||||
let podfilePath = fileHandler.currentPath.appending(component: "Podfile")
|
||||
|
||||
// When
|
||||
let result = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(result.contains(LintingIssue(reason: "The Podfile at path \(podfilePath) referenced by some projects does not exist", severity: .error)))
|
||||
}
|
||||
|
||||
func test_lint_when_frameworks_are_missing() throws {
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import Foundation
|
||||
import TuistCore
|
||||
import XCTest
|
||||
|
||||
@testable import TuistCoreTesting
|
||||
@testable import TuistGenerator
|
||||
|
||||
final class CocoaPodsInteractorErrorTests: XCTestCase {
|
||||
func test_type() {
|
||||
XCTAssertEqual(CocoaPodsInteractorError.cocoapodsNotFound.type, .abort)
|
||||
XCTAssertEqual(CocoaPodsInteractorError.outdatedRepository.type, .abort)
|
||||
}
|
||||
|
||||
func test_description() {
|
||||
XCTAssertEqual(CocoaPodsInteractorError.cocoapodsNotFound.description, "CocoaPods was not found either in Bundler nor in the environment")
|
||||
XCTAssertEqual(CocoaPodsInteractorError.outdatedRepository.description, "The installation of CocoaPods dependencies might have failed because the CocoaPods repository is outdated")
|
||||
}
|
||||
}
|
||||
|
||||
final class CocoaPodsInteractorTests: XCTestCase {
|
||||
var printer: MockPrinter!
|
||||
var system: MockSystem!
|
||||
var subject: CocoaPodsInteractor!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
printer = MockPrinter()
|
||||
system = MockSystem()
|
||||
subject = CocoaPodsInteractor(printer: printer, system: system)
|
||||
}
|
||||
|
||||
func test_install_when_cocoapods_cannot_be_found() {
|
||||
// Given
|
||||
system.errorCommand(["bundle", "show", "cocoapods"])
|
||||
system.whichStub = { _ in
|
||||
throw NSError.test()
|
||||
}
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
let cocoapods = CocoaPodsNode.test()
|
||||
cache.add(cocoapods: cocoapods)
|
||||
|
||||
// Then
|
||||
XCTAssertThrowsError(try subject.install(graph: graph)) {
|
||||
XCTAssertEqual($0 as? CocoaPodsInteractorError, CocoaPodsInteractorError.cocoapodsNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func test_install_when_theCocoaPodsFromBundlerCanBeUsed() throws {
|
||||
// Given
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
let cocoapods = CocoaPodsNode.test()
|
||||
cache.add(cocoapods: cocoapods)
|
||||
|
||||
system.succeedCommand(["bundle", "show", "cocoapods"])
|
||||
system.succeedCommand(["bundle", "exec", "pod", "install", "--project-directory=\(cocoapods.path.pathString)"])
|
||||
|
||||
// When
|
||||
try subject.install(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(printer.printSectionArgs.contains("Installing CocoaPods dependencies defined in \(cocoapods.podfilePath)"))
|
||||
}
|
||||
|
||||
func test_install_when_theCocoaPodsFromTheSystemCanBeUsed() throws {
|
||||
// Given
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
let cocoapods = CocoaPodsNode.test()
|
||||
cache.add(cocoapods: cocoapods)
|
||||
|
||||
system.errorCommand(["bundle", "show", "cocoapods"])
|
||||
system.whichStub = {
|
||||
if $0 == "pod" { return "/path/to/pod" }
|
||||
else { throw NSError.test() }
|
||||
}
|
||||
system.succeedCommand(["pod", "install", "--project-directory=\(cocoapods.path.pathString)"])
|
||||
|
||||
// When
|
||||
try subject.install(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(printer.printSectionArgs.contains("Installing CocoaPods dependencies defined in \(cocoapods.podfilePath)"))
|
||||
}
|
||||
|
||||
func test_install_when_theCocoaPodsSpecsRepoIsOutdated() throws {
|
||||
// Given
|
||||
let cache = GraphLoaderCache()
|
||||
let graph = Graph.test(cache: cache)
|
||||
let cocoapods = CocoaPodsNode.test()
|
||||
cache.add(cocoapods: cocoapods)
|
||||
|
||||
system.succeedCommand(["bundle", "show", "cocoapods"])
|
||||
system.errorCommand(["bundle", "exec", "pod", "install", "--project-directory=\(cocoapods.path.pathString)"], error: "[!] CocoaPods could not find compatible versions for pod")
|
||||
system.succeedCommand(["bundle", "exec", "pod", "install", "--project-directory=\(cocoapods.path.pathString)", "--repo-update"])
|
||||
|
||||
// When
|
||||
try subject.install(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(printer.printWarningArgs.contains("The local CocoaPods specs repository is outdated. Re-running 'pod install' updating the repository."))
|
||||
XCTAssertTrue(printer.printSectionArgs.contains("Installing CocoaPods dependencies defined in \(cocoapods.podfilePath)"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import Foundation
|
||||
|
||||
@testable import TuistGenerator
|
||||
|
||||
final class MockCocoaPodsInteractor: CocoaPodsInteracting {
|
||||
var installArgs: [Graphing] = []
|
||||
var installStub: Error?
|
||||
|
||||
func install(graph: Graphing) throws {
|
||||
installArgs.append(graph)
|
||||
if let error = installStub { throw error }
|
||||
}
|
||||
}
|
|
@ -296,6 +296,21 @@ class GeneratorModelLoaderTest: XCTestCase {
|
|||
assert(settings: model, matches: manifest, at: path)
|
||||
}
|
||||
|
||||
func test_dependency_when_cocoapods() throws {
|
||||
// Given
|
||||
let dependency = TargetDependency.cocoapods(path: "./path/to/project")
|
||||
|
||||
// When
|
||||
let got = TuistGenerator.Dependency.from(manifest: dependency)
|
||||
|
||||
// Then
|
||||
guard case let .cocoapods(path) = got else {
|
||||
XCTFail("Dependency should be cocoapods")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(path, RelativePath("./path/to/project"))
|
||||
}
|
||||
|
||||
func test_headers() throws {
|
||||
// Given
|
||||
try fileHandler.createFiles([
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
name: Dependencies
|
||||
---
|
||||
|
||||
import Message from '../components/message'
|
||||
|
||||
# Dependencies
|
||||
|
||||
**Setting up dependencies in Xcode projects isn't straightforward**. When dependencies have transitive dependencies things get complicated because it requires changes in the targets that are part of the branch where the transitive dependency is. To illustrate that, think about an app, depending on a dynamic framework `Search`, which has no dependencies. If at some point in the future we add a new dynamic framework `Core`, on which `Search` depends, well need to update not only `Search`, but the app to embed the framework into the product.
|
||||
|
@ -76,6 +78,27 @@ It defines a dependency with a pre-compiled library. It allows specifying the pa
|
|||
|
||||
It defines a dependency on a system library (`.tbd`) or framework (`.framework`) and optionally if it is `required` or `optional` (i.e. gets weakly linked).
|
||||
|
||||
-----
|
||||
## CocoaPods dependencies
|
||||
|
||||
Targets can indicate that they have [CocoaPods](https://cocoapods.org) dependencies defined in a `Podfile`:
|
||||
|
||||
```swift
|
||||
.cocoapods(path: ".") // Expects a Podfile in the directory of the target's project
|
||||
```
|
||||
|
||||
Tuist looks up CocoaPods using Bundler. If it's not defined, it falls back to the system's CocoaPods. If CocoaPods can't be found in the environment, the installation of the dependencies will fail.
|
||||
|
||||
<Message
|
||||
info
|
||||
title="Repository update"
|
||||
description="The underlying 'pod install' is executed with the '--update-repo' argument to ensure the local repository of pod specs is up to date."
|
||||
/>
|
||||
<Message
|
||||
warning
|
||||
title="Podfile validation"
|
||||
description="Tuist does not parse the CocoaPods dependency graph nor runs any validation. It's the user responsibility ensure the right format of the 'Podfile'."
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
As we mentioned, the beauty of defining your dependencies with Tuist is that when you generate the project, things are set up and ready for you to successfully compile your targets.
|
|
@ -44,7 +44,7 @@ Feature: Generate a new project using Tuist
|
|||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture invalid_workspace_manifest_name into the working directory
|
||||
Then tuist generates reports error "❌ Error: Manifest not found at path ${ARG_PATH}"
|
||||
Then tuist generates reports error "Error: Manifest not found at path ${ARG_PATH}"
|
||||
|
||||
Scenario: The project is an iOS application with frameworks and tests (ios_app_with_static_libraries)
|
||||
Given that tuist is available
|
||||
|
@ -160,3 +160,10 @@ Scenario: The project is an iOS application with multiple configurations (ios_ap
|
|||
Then the scheme Framework2 has a build setting CUSTOM_FLAG with value "Debug" for the configuration Debug
|
||||
Then the scheme Framework2 has a build setting CUSTOM_FLAG with value "Target.Beta" for the configuration Beta
|
||||
Then the scheme Framework2 has a build setting CUSTOM_FLAG with value "Release" for the configuration Release
|
||||
|
||||
Scenario: The project is an iOS application with CocoaPods dependencies (ios_app_with_pods)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture ios_app_with_pods into the working directory
|
||||
Then tuist generates the project
|
||||
Then I should be able to build the scheme App
|
|
@ -9,7 +9,7 @@ Contains a single file `Workspac.swift`, incorrectly named workspace manifest fi
|
|||
|
||||
## ios_app_with_custom_workspace
|
||||
|
||||
Contains a few projects and a `Workspace.swift` manifest file.
|
||||
Contains a few projects and a `Workspace.swift` manifest file.
|
||||
|
||||
The workspace manifest defines:
|
||||
|
||||
|
@ -20,7 +20,7 @@ The workspace manifest defines:
|
|||
The App's project manifest leverages `additionalFiles` tha defines:
|
||||
|
||||
- glob patterns to include documentation files
|
||||
- Includes a swift `Danger.swift` file that shouldn't get included in any buid phase
|
||||
- Includes a swift `Danger.swift` file that shouldn't get included in any buid phase
|
||||
- folder references to a directory with json files
|
||||
|
||||
## ios_app_with_tests
|
||||
|
@ -49,9 +49,10 @@ Workspace:
|
|||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> Framework1
|
||||
- App -> Framework2
|
||||
- Framework1 -> Framework2
|
||||
|
||||
- App -> Framework1
|
||||
- App -> Framework2
|
||||
- Framework1 -> Framework2
|
||||
|
||||
## ios_app_with_framework_and_resources
|
||||
|
||||
|
@ -67,8 +68,9 @@ Workspace:
|
|||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> Framework1
|
||||
|
||||
|
||||
- App -> Framework1
|
||||
|
||||
## ios_app_with_framework_linking_static_framework
|
||||
|
||||
An example project demonstrating an iOS application linking a dynamic framework which itself depends on a static framework with transitive static dependencies.
|
||||
|
@ -95,10 +97,11 @@ Workspace:
|
|||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> Framework1
|
||||
- Framework1 -> Framework2
|
||||
- Framework1 -> Framework3
|
||||
- Framework3 -> Framework4
|
||||
|
||||
- App -> Framework1
|
||||
- Framework1 -> Framework2
|
||||
- Framework1 -> Framework3
|
||||
- Framework3 -> Framework4
|
||||
|
||||
## ios_app_with_multi_configs
|
||||
|
||||
|
@ -128,6 +131,7 @@ Workspace:
|
|||
```
|
||||
|
||||
A standalone C project is used to generate a prebuilt static library:
|
||||
|
||||
```
|
||||
- C:
|
||||
- C (static library iOS)
|
||||
|
@ -135,11 +139,12 @@ A standalone C project is used to generate a prebuilt static library:
|
|||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> A
|
||||
- A -> B
|
||||
- A -> prebuild C (libC.a)
|
||||
|
||||
Note: to re-create `libC.a` run `fixtures/ios_app_with_static_libraries/Modules/C/build.sh`
|
||||
- App -> A
|
||||
- A -> B
|
||||
- A -> prebuild C (libC.a)
|
||||
|
||||
Note: to re-create `libC.a` run `fixtures/ios_app_with_static_libraries/Modules/C/build.sh`
|
||||
|
||||
## ios_app_with_static_frameworks
|
||||
|
||||
|
@ -164,11 +169,12 @@ Workspace:
|
|||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> A
|
||||
- App -> C
|
||||
- A -> B
|
||||
- A -> C
|
||||
- C -> D
|
||||
|
||||
- App -> A
|
||||
- App -> C
|
||||
- A -> B
|
||||
- A -> C
|
||||
- C -> D
|
||||
|
||||
## ios_app_with_tests
|
||||
|
||||
|
@ -187,14 +193,19 @@ Workspace:
|
|||
```
|
||||
|
||||
A standalone Framework2 project is used to generate a prebuilt dynamic framework :
|
||||
|
||||
```
|
||||
- Framework2:
|
||||
- Framework2 (dynamic iOS framework)
|
||||
```
|
||||
|
||||
Dependencies:
|
||||
- App -> Framework1
|
||||
- Framework1 -> Framework2 (prebuilt)
|
||||
|
||||
Note: to re-create `Framework2.framework` run `fixtures/ios_app_with_transitive_framework/Framework2/build.sh`
|
||||
- App -> Framework1
|
||||
- Framework1 -> Framework2 (prebuilt)
|
||||
|
||||
Note: to re-create `Framework2.framework` run `fixtures/ios_app_with_transitive_framework/Framework2/build.sh`
|
||||
|
||||
## ios_app_with_pods
|
||||
|
||||
An iOS application with CocoaPods dependencies
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
|
||||
### Projects ###
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
||||
Pods/
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright ©. All rights reserved.</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,6 @@
|
|||
platform :ios, '12.0'
|
||||
use_frameworks!
|
||||
|
||||
target 'App' do
|
||||
pod 'RxSwift', '~> 5'
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
PODS:
|
||||
- RxSwift (5.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- RxSwift (~> 5)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
- RxSwift
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
RxSwift: 8b0671caa829a763bbce7271095859121cbd895f
|
||||
|
||||
PODFILE CHECKSUM: 3baa4c5df87f8aaf5441c016e9d9914ea1463c1f
|
||||
|
||||
COCOAPODS: 1.7.5
|
|
@ -0,0 +1,14 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(name: "App",
|
||||
targets: [
|
||||
Target(name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.app",
|
||||
infoPlist: "Info.plist",
|
||||
sources: ["Sources/**"],
|
||||
dependencies: [
|
||||
.cocoapods(path: ".")
|
||||
]),
|
||||
])
|
|
@ -0,0 +1,24 @@
|
|||
import UIKit
|
||||
import RxSwift
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
|
||||
) -> Bool {
|
||||
// To make sure RxSwift is available
|
||||
let observable = Observable.just("Test")
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let viewController = UIViewController()
|
||||
viewController.view.backgroundColor = .white
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue