Merge remote-tracking branch 'origin/master' into scaffold_init

This commit is contained in:
Marek Fořt 2020-03-22 17:19:27 +01:00
commit 32ec0e6b88
89 changed files with 2604 additions and 610 deletions

View File

@ -7,6 +7,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
### Changed
- Optimize `TargetNode`'s set operations https://github.com/tuist/tuist/pull/1095 by @kwridan
- Optimize `BuildPhaseGenerator`'s method of detecting assets and localized files https://github.com/tuist/tuist/pull/1094 by @kwridan
- Concurrent project generation https://github.com/tuist/tuist/pull/1096 by @kwridan
## 1.4.0

View File

@ -195,7 +195,7 @@ let package = Package(
),
.testTarget(
name: "TuistIntegrationTests",
dependencies: ["TuistGenerator", "TuistSupportTesting", "TuistSupport"]
dependencies: ["TuistGenerator", "TuistSupportTesting", "TuistSupport", "TuistCoreTesting"]
),
]
)

View File

@ -292,7 +292,7 @@ public class Graph: Graphing {
if targetNode.target.canLinkStaticProducts() {
let transitiveSystemLibraries = transitiveStaticTargetNodes(for: targetNode).flatMap {
$0.sdkDependencies.map {
GraphDependencyReference.sdk($0.path, $0.status)
GraphDependencyReference.sdk(path: $0.path, status: $0.status)
}
}
@ -300,7 +300,7 @@ public class Graph: Graphing {
}
let directSystemLibrariesAndFrameworks = targetNode.sdkDependencies.map {
GraphDependencyReference.sdk($0.path, $0.status)
GraphDependencyReference.sdk(path: $0.path, status: $0.status)
}
references = references.union(directSystemLibrariesAndFrameworks)
@ -309,8 +309,7 @@ public class Graph: Graphing {
let precompiledLibrariesAndFrameworks = targetNode.precompiledDependencies
.lazy
.map(\.path)
.map(GraphDependencyReference.absolute)
.map(GraphDependencyReference.init)
references = references.union(precompiledLibrariesAndFrameworks)
@ -382,13 +381,16 @@ public class Graph: Graphing {
var references: Set<GraphDependencyReference> = Set([])
let isDynamicAndLinkable = frameworkUsesDynamicLinking()
let isDynamicAndLinkable = { (node: PrecompiledNode) -> Bool in
if let framework = node as? FrameworkNode { return framework.linking == .dynamic }
if let xcframework = node as? XCFrameworkNode { return xcframework.linking == .dynamic }
return false
}
/// Precompiled frameworks
let precompiledFrameworks = findAll(targetNode: targetNode, test: isDynamicAndLinkable, skip: canEmbedProducts)
.lazy
.map(\.path)
.map(GraphDependencyReference.absolute)
.map(GraphDependencyReference.init)
references.formUnion(precompiledFrameworks)
@ -573,11 +575,4 @@ public class Graph: Graphing {
return validProducts.contains(targetNode.target.product)
}
// swiftlint:disable:next line_length
fileprivate func frameworkUsesDynamicLinking(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) -> (_ frameworkNode: PrecompiledNode) -> Bool { { frameworkNode in
let isDynamicLink = try? frameworkMetadataProvider.linking(precompiled: frameworkNode) == .dynamic
return isDynamicLink ?? false
}
}
}

View File

@ -2,14 +2,66 @@ import Basic
import Foundation
public enum GraphDependencyReference: Equatable, Comparable, Hashable {
case absolute(AbsolutePath)
case xcframework(
path: AbsolutePath,
infoPlist: XCFrameworkInfoPlist,
primaryBinaryPath: AbsolutePath,
binaryPath: AbsolutePath
)
case library(
path: AbsolutePath,
binaryPath: AbsolutePath,
linking: BinaryLinking,
architectures: [BinaryArchitecture],
product: Product
)
case framework(
path: AbsolutePath,
binaryPath: AbsolutePath,
isCarthage: Bool,
dsymPath: AbsolutePath?,
bcsymbolmapPaths: [AbsolutePath],
linking: BinaryLinking,
architectures: [BinaryArchitecture],
product: Product
)
case product(target: String, productName: String)
case sdk(AbsolutePath, SDKStatus)
case sdk(path: AbsolutePath, status: SDKStatus)
init(precompiledNode: PrecompiledNode) {
if let frameworkNode = precompiledNode as? FrameworkNode {
self = .framework(path: frameworkNode.path,
binaryPath: frameworkNode.binaryPath,
isCarthage: frameworkNode.isCarthage,
dsymPath: frameworkNode.dsymPath,
bcsymbolmapPaths: frameworkNode.bcsymbolmapPaths,
linking: frameworkNode.linking,
architectures: frameworkNode.architectures,
product: frameworkNode.product)
} else if let libraryNode = precompiledNode as? LibraryNode {
self = .library(path: libraryNode.path,
binaryPath: libraryNode.binaryPath,
linking: libraryNode.linking,
architectures: libraryNode.architectures,
product: libraryNode.product)
} else if let xcframeworkNode = precompiledNode as? XCFrameworkNode {
self = .xcframework(path: xcframeworkNode.path,
infoPlist: xcframeworkNode.infoPlist,
primaryBinaryPath: xcframeworkNode.primaryBinaryPath,
binaryPath: xcframeworkNode.binaryPath)
} else {
preconditionFailure("unsupported precompiled node")
}
}
public func hash(into hasher: inout Hasher) {
switch self {
case let .absolute(path):
hasher.combine(path)
case let .library(metadata):
hasher.combine(metadata.0)
case let .framework(metadata):
hasher.combine(metadata.0)
case let .xcframework(metadata):
hasher.combine(metadata.0)
case let .product(target, productName):
hasher.combine(target)
hasher.combine(productName)
@ -19,23 +71,30 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable {
}
}
public static func == (lhs: GraphDependencyReference, rhs: GraphDependencyReference) -> Bool {
switch (lhs, rhs) {
case let (.absolute(lhsPath), .absolute(rhsPath)):
return lhsPath == rhsPath
case let (.product(lhsTarget, lhsProductName), .product(rhsTarget, rhsProductName)):
return lhsTarget == rhsTarget && lhsProductName == rhsProductName
case let (.sdk(lhsPath, lhsStatus), .sdk(rhsPath, rhsStatus)):
return lhsPath == rhsPath && lhsStatus == rhsStatus
/// For dependencies that exists in the file system. This attribute returns the path to them.
public var path: AbsolutePath? {
switch self {
case let .framework(metadata):
return metadata.path
case let .library(metadata):
return metadata.path
case let .xcframework(metadata):
return metadata.path
case let .sdk(metadata):
return metadata.path
default:
return false
return nil
}
}
public static func < (lhs: GraphDependencyReference, rhs: GraphDependencyReference) -> Bool {
switch (lhs, rhs) {
case let (.absolute(lhsPath), .absolute(rhsPath)):
return lhsPath < rhsPath
case let (.framework(lhsMetadata), .framework(rhsMetadata)):
return lhsMetadata.path < rhsMetadata.path
case let (.xcframework(lhsMetadata), .xcframework(rhsMetadata)):
return lhsMetadata.path < rhsMetadata.path
case let (.library(lhsMetadata), .library(rhsMetadata)):
return lhsMetadata.path < rhsMetadata.path
case let (.product(lhsTarget, lhsProductName), .product(rhsTarget, rhsProductName)):
if lhsTarget == rhsTarget {
return lhsProductName < rhsProductName
@ -43,11 +102,25 @@ public enum GraphDependencyReference: Equatable, Comparable, Hashable {
return lhsTarget < rhsTarget
case let (.sdk(lhsPath, _), .sdk(rhsPath, _)):
return lhsPath < rhsPath
case (.sdk, .absolute):
case (.sdk, .framework):
return true
case (.sdk, .xcframework):
return true
case (.sdk, .product):
return true
case (.product, .absolute):
case (.sdk, .library):
return true
case (.product, .framework):
return true
case (.product, .xcframework):
return true
case (.product, .library):
return true
case (.library, .framework):
return true
case (.library, .xcframework):
return true
case (.framework, .xcframework):
return true
default:
return false

View File

@ -24,10 +24,32 @@ public class GraphLoader: GraphLoading {
fileprivate let modelLoader: GeneratorModelLoading
/// Utility to load framework nodes by parsing their information from disk.
fileprivate let frameworkNodeLoader: FrameworkNodeLoading
/// Utility to load xcframework nodes by parsing their information from disk.
fileprivate let xcframeworkNodeLoader: XCFrameworkNodeLoading
/// Utility to load library nodes by parsing their information from disk.
fileprivate let libraryNodeLoader: LibraryNodeLoading
// MARK: - Init
public init(modelLoader: GeneratorModelLoading) {
public convenience init(modelLoader: GeneratorModelLoading) {
self.init(modelLoader: modelLoader,
frameworkNodeLoader: FrameworkNodeLoader(),
xcframeworkNodeLoader: XCFrameworkNodeLoader(),
libraryNodeLoader: LibraryNodeLoader())
}
public init(modelLoader: GeneratorModelLoading,
frameworkNodeLoader: FrameworkNodeLoading,
xcframeworkNodeLoader: XCFrameworkNodeLoading,
libraryNodeLoader: LibraryNodeLoading) {
self.modelLoader = modelLoader
self.frameworkNodeLoader = frameworkNodeLoader
self.xcframeworkNodeLoader = xcframeworkNodeLoader
self.libraryNodeLoader = libraryNodeLoader
}
// MARK: - GraphLoading
@ -216,7 +238,7 @@ public class GraphLoader: GraphLoading {
/// - graphLoaderCache: Graph loader cache.
fileprivate func loadFrameworkNode(frameworkPath: AbsolutePath, graphLoaderCache: GraphLoaderCaching) throws -> FrameworkNode {
if let frameworkNode = graphLoaderCache.precompiledNode(frameworkPath) as? FrameworkNode { return frameworkNode }
let framewokNode = FrameworkNode(path: frameworkPath)
let framewokNode = try frameworkNodeLoader.load(path: frameworkPath)
graphLoaderCache.add(precompiledNode: framewokNode)
return framewokNode
}
@ -231,26 +253,11 @@ public class GraphLoader: GraphLoading {
swiftModuleMap: AbsolutePath?,
libraryPath: AbsolutePath,
graphLoaderCache: GraphLoaderCaching) throws -> LibraryNode {
// TODO: Validate using linters
if !FileHandler.shared.exists(libraryPath) {
throw GraphLoadingError.missingFile(libraryPath)
}
if let libraryNode = graphLoaderCache.precompiledNode(libraryPath) as? LibraryNode { return libraryNode }
let libraryNode = try libraryNodeLoader.load(path: libraryPath,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap)
// TODO: Validate using linters
if !FileHandler.shared.exists(publicHeaders) {
throw GraphLoadingError.missingFile(publicHeaders)
}
// TODO: Validate using linters
if let swiftModuleMap = swiftModuleMap {
if !FileHandler.shared.exists(swiftModuleMap) {
throw GraphLoadingError.missingFile(swiftModuleMap)
}
}
let libraryNode = LibraryNode(path: libraryPath,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap)
graphLoaderCache.add(precompiledNode: libraryNode)
return libraryNode
}
@ -276,10 +283,12 @@ public class GraphLoader: GraphLoading {
/// - Parameters:
/// - xcframeworkPath: Path to the .xcframework.
/// - graphLoaderCache: Graph loader cache.
fileprivate func loadXCFrameworkNode(
path: AbsolutePath,
graphLoaderCache: GraphLoaderCaching
) throws -> XCFrameworkNode {
try XCFrameworkParser.parse(path: path, cache: graphLoaderCache)
fileprivate func loadXCFrameworkNode(path: AbsolutePath, graphLoaderCache: GraphLoaderCaching) throws -> XCFrameworkNode {
if let cachedXCFramework = graphLoaderCache.precompiledNode(path) as? XCFrameworkNode {
return cachedXCFramework
}
let xcframework = try xcframeworkNodeLoader.load(path: path)
graphLoaderCache.add(precompiledNode: xcframework)
return xcframework
}
}

View File

@ -3,6 +3,31 @@ import Foundation
import TuistSupport
public class FrameworkNode: PrecompiledNode {
/// Path to the associated .dSYM
public let dsymPath: AbsolutePath?
/// Paths to the bcsymbolmap files.
public let bcsymbolmapPaths: [AbsolutePath]
/// Returns the type of linking
public let linking: BinaryLinking
/// The architectures supported by the binary.
public let architectures: [BinaryArchitecture]
/// Framework dependencies.
public let dependencies: [FrameworkNode]
/// Returns the type of product.
public var product: Product {
if linking == .static {
return .staticFramework
} else {
return .framework
}
}
/// Returns true if it's a Carthage framework.
public var isCarthage: Bool {
path.pathString.contains("Carthage/Build")
}
@ -12,15 +37,26 @@ public class FrameworkNode: PrecompiledNode {
FrameworkNode.binaryPath(frameworkPath: path)
}
init(path: AbsolutePath,
dsymPath: AbsolutePath?,
bcsymbolmapPaths: [AbsolutePath],
linking: BinaryLinking,
architectures: [BinaryArchitecture] = [],
dependencies: [FrameworkNode] = []) {
self.dsymPath = dsymPath
self.bcsymbolmapPaths = bcsymbolmapPaths
self.linking = linking
self.architectures = architectures
self.dependencies = dependencies
super.init(path: path)
}
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let metadataProvider = FrameworkMetadataProvider()
try container.encode(path.pathString, forKey: .path)
try container.encode(name, forKey: .name)
try container.encode(try metadataProvider.product(framework: self), forKey: .product)
let archs = try metadataProvider.architectures(precompiled: self)
try container.encode(archs.map(\.rawValue), forKey: .architectures)
try container.encode(product, forKey: .product)
try container.encode(architectures.map(\.rawValue), forKey: .architectures)
try container.encode("precompiled", forKey: .type)
}

View File

@ -5,16 +5,38 @@ import TuistSupport
public class LibraryNode: PrecompiledNode {
// MARK: - Attributes
/// Directory that contains the public headers of the library.
let publicHeaders: AbsolutePath
/// Path to the Swift module map file.
let swiftModuleMap: AbsolutePath?
/// List of supported architectures.
let architectures: [BinaryArchitecture]
/// Type of linking supported by the binary.
let linking: BinaryLinking
/// Library product.
var product: Product {
if linking == .static {
return .staticLibrary
} else {
return .dynamicLibrary
}
}
// MARK: - Init
init(path: AbsolutePath,
publicHeaders: AbsolutePath,
architectures: [BinaryArchitecture],
linking: BinaryLinking,
swiftModuleMap: AbsolutePath? = nil) {
self.publicHeaders = publicHeaders
self.swiftModuleMap = swiftModuleMap
self.architectures = architectures
self.linking = linking
super.init(path: path)
}
@ -22,6 +44,8 @@ public class LibraryNode: PrecompiledNode {
super.hash(into: &hasher)
hasher.combine(publicHeaders)
hasher.combine(swiftModuleMap)
hasher.combine(architectures)
hasher.combine(linking)
}
static func == (lhs: LibraryNode, rhs: LibraryNode) -> Bool {
@ -35,6 +59,8 @@ public class LibraryNode: PrecompiledNode {
return path == otherLibraryNode.path
&& swiftModuleMap == otherLibraryNode.swiftModuleMap
&& publicHeaders == otherLibraryNode.publicHeaders
&& architectures == otherLibraryNode.architectures
&& linking == otherLibraryNode.linking
}
public override var binaryPath: AbsolutePath {
@ -43,13 +69,10 @@ public class LibraryNode: PrecompiledNode {
public override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let metadataProvider = LibraryMetadataProvider()
try container.encode(path.pathString, forKey: .path)
try container.encode(name, forKey: .name)
try container.encode(try metadataProvider.product(library: self), forKey: .product)
let archs = try metadataProvider.architectures(precompiled: self)
try container.encode(archs.map(\.rawValue), forKey: .architectures)
try container.encode(product, forKey: .product)
try container.encode(architectures.map(\.rawValue), forKey: .architectures)
try container.encode("precompiled", forKey: .type)
}
}

View File

@ -3,33 +3,55 @@ import Foundation
import TuistSupport
public class XCFrameworkNode: PrecompiledNode {
/// Coding keys.
enum XCFrameworkNodeCodingKeys: String, CodingKey {
case libraries
case linking
case type
case path
case name
case infoPlist = "info_plist"
}
public let libraries: [XCFrameworkInfoPlist.Library]
/// The xcframework's Info.plist content.
public let infoPlist: XCFrameworkInfoPlist
/// Path to the primary binary.
public let primaryBinaryPath: AbsolutePath
public init(
path: AbsolutePath,
libraries: [XCFrameworkInfoPlist.Library],
primaryBinaryPath: AbsolutePath
) {
self.libraries = libraries
/// Returns the type of linking
public let linking: BinaryLinking
/// List of other .xcframeworks this xcframework depends on.
public let dependencies: [XCFrameworkNode]
/// Path to the binary.
public override var binaryPath: AbsolutePath { primaryBinaryPath }
/// Initializes the node with its attributes.
/// - Parameters:
/// - path: Path to the .xcframework.
/// - infoPlist: The xcframework's Info.plist content.
/// - primaryBinaryPath: Path to the primary binary.
/// - linking: Returns the type of linking.
/// - dependencies: List of other .xcframeworks this xcframework depends on.
public init(path: AbsolutePath,
infoPlist: XCFrameworkInfoPlist,
primaryBinaryPath: AbsolutePath,
linking: BinaryLinking,
dependencies: [XCFrameworkNode] = []) {
self.infoPlist = infoPlist
self.linking = linking
self.primaryBinaryPath = primaryBinaryPath
self.dependencies = dependencies
super.init(path: path)
}
public override var binaryPath: AbsolutePath {
primaryBinaryPath
}
public override func encode(to encoder: Encoder) throws {
var parentContainer = encoder.container(keyedBy: CodingKeys.self)
try parentContainer.encode(path.pathString, forKey: .path)
try parentContainer.encode(name, forKey: .name)
try parentContainer.encode("precompiled", forKey: .type)
var container = encoder.container(keyedBy: XCFrameworkNodeCodingKeys.self)
try container.encode(libraries, forKey: .libraries)
try container.encode(path.pathString, forKey: .path)
try container.encode(name, forKey: .name)
try container.encode(linking, forKey: .linking)
try container.encode("xcframework", forKey: .type)
try container.encode(infoPlist, forKey: .infoPlist)
}
}

View File

@ -2,48 +2,30 @@ import Basic
import Foundation
import TuistSupport
public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
/// Given the path to a framework, it returns the path to its dSYMs if they exist
/// in the same framework directory.
/// - Parameter frameworkPath: Path to the .framework directory.
func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath?
/// Given a framework, it returns the path to its dSYMs if they exist
/// in the same framework directory.
/// - Parameter framework: Framework instance.
func dsymPath(framework: FrameworkNode) -> AbsolutePath?
/// Given the path to a framework, it returns the list of .bcsymbolmap files that
/// are associated to the framework and that are present in the same directory.
/// - Parameter frameworkPath: Path to the .framework directory.
func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath]
/// Given a framework, it returns the list of .bcsymbolmap files that
/// are associated to the framework and that are present in the same directory.
/// - Parameter framework: Framework instance.
func bcsymbolmapPaths(framework: FrameworkNode) throws -> [AbsolutePath]
/// Returns the product for the framework at the given path.
/// - Parameter frameworkPath: Path to the .framework directory.
func product(frameworkPath: AbsolutePath) throws -> Product
/// Returns the product for the given framework.
/// - Parameter framework: Framework instance.
func product(framework: FrameworkNode) throws -> Product
}
public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
let path = AbsolutePath("\(frameworkPath.pathString).dSYM")
if FileHandler.shared.exists(path) { return path }
return nil
}
public func dsymPath(framework: FrameworkNode) -> AbsolutePath? {
dsymPath(frameworkPath: framework.path)
}
public func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
let uuids = try self.uuids(binaryPath: binaryPath)
return uuids
@ -52,11 +34,7 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame
.sorted()
}
public func bcsymbolmapPaths(framework: FrameworkNode) throws -> [AbsolutePath] {
try bcsymbolmapPaths(frameworkPath: framework.path)
}
public func product(frameworkPath: AbsolutePath) throws -> Product {
func product(frameworkPath: AbsolutePath) throws -> Product {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
switch try linking(binaryPath: binaryPath) {
case .dynamic:
@ -65,8 +43,4 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame
return .staticFramework
}
}
public func product(framework: FrameworkNode) throws -> Product {
try product(frameworkPath: framework.path)
}
}

View File

@ -3,26 +3,18 @@ import Foundation
import TuistSupport
protocol LibraryMetadataProviding: PrecompiledMetadataProviding {
/// Returns the product for the library at the given path.
/// - Parameter libraryPath: Path to the library.
func product(libraryPath: AbsolutePath) throws -> Product
/// Returns the product for the given library.
/// - Parameter library: Library instance.
func product(library: LibraryNode) throws -> Product
}
final class LibraryMetadataProvider: PrecompiledMetadataProvider, LibraryMetadataProviding {
func product(libraryPath: AbsolutePath) throws -> Product {
switch try linking(binaryPath: libraryPath) {
func product(library: LibraryNode) throws -> Product {
switch try linking(binaryPath: library.path) {
case .dynamic:
return .dynamicLibrary
case .static:
return .staticLibrary
}
}
func product(library: LibraryNode) throws -> Product {
try product(libraryPath: library.path)
}
}

View File

@ -31,42 +31,25 @@ enum PrecompiledMetadataProviderError: FatalError, Equatable {
}
}
public protocol PrecompiledMetadataProviding {
protocol PrecompiledMetadataProviding {
/// It returns the supported architectures of the binary at the given path.
/// - Parameter binaryPath: Binary path.
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture]
/// It returns the supported architectures of the precompiled framework or library.
/// - Parameter precompiled: Precompiled framework or library.
func architectures(precompiled: PrecompiledNode) throws -> [BinaryArchitecture]
/// Return how other binaries should link the binary at the given path.
/// - Parameter binaryPath: Path to the binary.
func linking(binaryPath: AbsolutePath) throws -> BinaryLinking
/// Return how other binaries should link the given precompiled framework or library.
/// - Parameter precompiled: Precompiled framework or library.
func linking(precompiled: PrecompiledNode) throws -> BinaryLinking
/// It uses 'dwarfdump' to dump the UUIDs of each architecture.
/// The UUIDs allows us to know which .bcsymbolmap files belong to this binary.
/// - Parameter binaryPath: Path to the binary.
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID>
/// It uses 'dwarfdump' to dump the UUIDs of each architecture.
/// The UUIDs allows us to know which .bcsymbolmap files belong to this binary.
/// - Parameter precompiled: Precompiled framework or library.
func uuids(precompiled: PrecompiledNode) throws -> Set<UUID>
}
public class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
public init() {}
public func architectures(precompiled: PrecompiledNode) throws -> [BinaryArchitecture] {
try architectures(binaryPath: precompiled.binaryPath)
}
public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
let result = try System.shared.capture("/usr/bin/lipo", "-info", binaryPath.pathString).spm_chuzzle() ?? ""
let regexes = [
// Non-fat file: path is architecture: x86_64
@ -87,20 +70,12 @@ public class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
return architectures
}
public func linking(precompiled: PrecompiledNode) throws -> BinaryLinking {
try linking(binaryPath: precompiled.binaryPath)
}
public func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
let result = try System.shared.capture("/usr/bin/file", binaryPath.pathString).spm_chuzzle() ?? ""
return result.contains("dynamically linked") ? .dynamic : .static
}
public func uuids(precompiled: PrecompiledNode) throws -> Set<UUID> {
try uuids(binaryPath: precompiled.binaryPath)
}
public func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
let output = try System.shared.capture(["/usr/bin/xcrun", "dwarfdump", "--uuid", binaryPath.pathString])
// UUIDs are letters, decimals, or hyphens.
var uuidCharacterSet = CharacterSet()

View File

@ -28,48 +28,47 @@ enum XCFrameworkMetadataProviderError: FatalError, Equatable {
}
}
public protocol XCFrameworkMetadataProviding {
/// Returns the available libraries for the xcframework at the given path.
/// - Parameter frameworkPath: Path to the xcframework.
func libraries(frameworkPath: AbsolutePath) throws -> [XCFrameworkInfoPlist.Library]
protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding {
/// Returns the info.plist of the xcframework at the given path.
/// - Parameter xcframeworkPath: Path to the xcframework.
func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist
/// Given a framework path and libraries it returns the path to its binary.
/// - Parameter frameworkPath: Framework path.
/// - Parameter xcframeworkPath: Path to the .xcframework
/// - Parameter libraries: Framework available libraries
func binaryPath(frameworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath
func binaryPath(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath
}
public class XCFrameworkMetadataProvider: XCFrameworkMetadataProviding {
public func libraries(frameworkPath: AbsolutePath) throws -> [XCFrameworkInfoPlist.Library] {
class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCFrameworkMetadataProviding {
func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist {
let fileHandler = FileHandler.shared
let infoPlist = frameworkPath.appending(component: "Info.plist")
let infoPlist = xcframeworkPath.appending(component: "Info.plist")
guard fileHandler.exists(infoPlist) else {
throw XCFrameworkMetadataProviderError.missingRequiredFile(infoPlist)
}
let config: XCFrameworkInfoPlist = try fileHandler.readPlistFile(infoPlist)
return config.libraries
return try fileHandler.readPlistFile(infoPlist)
}
public func binaryPath(frameworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath {
func binaryPath(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath {
let archs: [BinaryArchitecture] = [.arm64, .x8664]
guard let library = libraries.first(where: { !$0.architectures.filter(archs.contains).isEmpty }) else {
throw XCFrameworkMetadataProviderError.supportedArchitectureReferencesNotFound(frameworkPath)
throw XCFrameworkMetadataProviderError.supportedArchitectureReferencesNotFound(xcframeworkPath)
}
let binaryName = frameworkPath.basenameWithoutExt
let binaryName = xcframeworkPath.basenameWithoutExt
let binaryPath: AbsolutePath
switch library.path.extension {
case "framework":
binaryPath = AbsolutePath(library.identifier, relativeTo: frameworkPath)
binaryPath = AbsolutePath(library.identifier, relativeTo: xcframeworkPath)
.appending(RelativePath(library.path.pathString))
.appending(component: binaryName)
case "a":
binaryPath = AbsolutePath(library.identifier, relativeTo: frameworkPath)
binaryPath = AbsolutePath(library.identifier, relativeTo: xcframeworkPath)
.appending(RelativePath(library.path.pathString))
default:
throw XCFrameworkMetadataProviderError.fileTypeNotRecognised(file: library.path, frameworkName: frameworkPath.basename)
throw XCFrameworkMetadataProviderError.fileTypeNotRecognised(file: library.path, frameworkName: xcframeworkPath.basename)
}
return binaryPath
}

View File

@ -8,6 +8,6 @@ public enum BinaryArchitecture: String, Codable {
case arm64
}
public enum BinaryLinking: String {
public enum BinaryLinking: String, Codable {
case `static`, dynamic
}

View File

@ -1,11 +1,13 @@
import Basic
import Foundation
/// It represents th Info.plist contained in an .xcframework bundle.
public struct XCFrameworkInfoPlist: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case libraries = "AvailableLibraries"
}
/// It represents a library inside an .xcframework
public struct Library: Codable, Equatable {
enum CodingKeys: String, CodingKey {
case identifier = "LibraryIdentifier"
@ -13,12 +15,18 @@ public struct XCFrameworkInfoPlist: Codable, Equatable {
case architectures = "SupportedArchitectures"
}
/// It represents the library's platform.
public enum Platform: String, Codable {
case ios
}
/// Library identifier.
public let identifier: String
/// Path to the library.
public let path: RelativePath
/// Architectures the binary is built for.
public let architectures: [BinaryArchitecture]
public func encode(to encoder: Encoder) throws {
@ -29,5 +37,6 @@ public struct XCFrameworkInfoPlist: Codable, Equatable {
}
}
/// List of libraries that are part of the .xcframework.
public let libraries: [Library]
}

View File

@ -0,0 +1,58 @@
import Basic
import Foundation
import TuistSupport
enum FrameworkNodeLoaderError: FatalError, Equatable {
case frameworkNotFound(AbsolutePath)
/// Error type.
var type: ErrorType {
switch self {
case .frameworkNotFound:
return .abort
}
}
/// Error description
var description: String {
switch self {
case let .frameworkNotFound(path):
return "Couldn't find framework at \(path.pathString)"
}
}
}
public protocol FrameworkNodeLoading {
/// Reads an existing framework and returns its in-memory representation, FrameworkNode.
/// - Parameter path: Path to the .framework.
func load(path: AbsolutePath) throws -> FrameworkNode
}
final class FrameworkNodeLoader: FrameworkNodeLoading {
/// Framework metadata provider.
fileprivate let frameworkMetadataProvider: FrameworkMetadataProviding
/// Initializes the loader with its attributes.
/// - Parameter frameworkMetadataProvider: Framework metadata provider.
init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
self.frameworkMetadataProvider = frameworkMetadataProvider
}
func load(path: AbsolutePath) throws -> FrameworkNode {
guard FileHandler.shared.exists(path) else {
throw FrameworkNodeLoaderError.frameworkNotFound(path)
}
let binaryPath = FrameworkNode.binaryPath(frameworkPath: path)
let dsymsPath = frameworkMetadataProvider.dsymPath(frameworkPath: path)
let bcsymbolmapPaths = try frameworkMetadataProvider.bcsymbolmapPaths(frameworkPath: path)
let linking = try frameworkMetadataProvider.linking(binaryPath: binaryPath)
let architectures = try frameworkMetadataProvider.architectures(binaryPath: binaryPath)
return FrameworkNode(path: path,
dsymPath: dsymsPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures)
}
}

View File

@ -0,0 +1,77 @@
import Basic
import Foundation
import TuistSupport
enum LibraryNodeLoaderError: FatalError, Equatable {
case libraryNotFound(AbsolutePath)
case publicHeadersNotFound(AbsolutePath)
case swiftModuleMapNotFound(AbsolutePath)
/// Error type.
var type: ErrorType {
switch self {
case .libraryNotFound: return .abort
case .publicHeadersNotFound: return .abort
case .swiftModuleMapNotFound: return .abort
}
}
/// Error description
var description: String {
switch self {
case let .libraryNotFound(path):
return "The library \(path.pathString) does not exist"
case let .publicHeadersNotFound(path):
return "The public headers directory \(path.pathString) does not exist"
case let .swiftModuleMapNotFound(path):
return "The Swift modulemap file \(path.pathString) does not exist"
}
}
}
public protocol LibraryNodeLoading {
/// Reads an existing library and returns its in-memory representation, LibraryNode.
/// - Parameters:
/// - path: Path to the library.
/// - publicHeaders: Path to the directorythat contains the public headers.
/// - swiftModuleMap: Path to the Swift modulemap file.
func load(path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?) throws -> LibraryNode
}
final class LibraryNodeLoader: LibraryNodeLoading {
/// Library metadata provider.
fileprivate let libraryMetadataProvider: LibraryMetadataProviding
/// Initializes the loader with its attributes.
/// - Parameter libraryMetadataProvider: Library metadata provider.
init(libraryMetadataProvider: LibraryMetadataProviding = LibraryMetadataProvider()) {
self.libraryMetadataProvider = libraryMetadataProvider
}
func load(path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?) throws -> LibraryNode {
if !FileHandler.shared.exists(path) {
throw LibraryNodeLoaderError.libraryNotFound(path)
}
if !FileHandler.shared.exists(publicHeaders) {
throw LibraryNodeLoaderError.publicHeadersNotFound(publicHeaders)
}
if let swiftModuleMap = swiftModuleMap {
if !FileHandler.shared.exists(swiftModuleMap) {
throw LibraryNodeLoaderError.swiftModuleMapNotFound(swiftModuleMap)
}
}
let architectures = try libraryMetadataProvider.architectures(binaryPath: path)
let linking = try libraryMetadataProvider.linking(binaryPath: path)
return LibraryNode(path: path,
publicHeaders: publicHeaders,
architectures: architectures,
linking: linking,
swiftModuleMap: swiftModuleMap)
}
}

View File

@ -0,0 +1,55 @@
import Basic
import Foundation
import TuistSupport
enum XCFrameworkNodeLoaderError: FatalError, Equatable {
case xcframeworkNotFound(AbsolutePath)
/// Error type.
var type: ErrorType {
switch self {
case .xcframeworkNotFound:
return .abort
}
}
/// Error description
var description: String {
switch self {
case let .xcframeworkNotFound(path):
return "Couldn't find xcframework at \(path.pathString)"
}
}
}
public protocol XCFrameworkNodeLoading {
/// Reads an existing xcframework and returns its in-memory representation, XCFrameworkNode..
/// - Parameter path: Path to the .xcframework.
func load(path: AbsolutePath) throws -> XCFrameworkNode
}
final class XCFrameworkNodeLoader: XCFrameworkNodeLoading {
/// xcframework metadata provider.
fileprivate let xcframeworkMetadataProvider: XCFrameworkMetadataProviding
/// Initializes the loader with its attributes.
/// - Parameter xcframeworkMetadataProvider: xcframework metadata provider.
init(xcframeworkMetadataProvider: XCFrameworkMetadataProviding = XCFrameworkMetadataProvider()) {
self.xcframeworkMetadataProvider = xcframeworkMetadataProvider
}
func load(path: AbsolutePath) throws -> XCFrameworkNode {
guard FileHandler.shared.exists(path) else {
throw XCFrameworkNodeLoaderError.xcframeworkNotFound(path)
}
let infoPlist = try xcframeworkMetadataProvider.infoPlist(xcframeworkPath: path)
let primaryBinaryPath = try xcframeworkMetadataProvider.binaryPath(xcframeworkPath: path,
libraries: infoPlist.libraries)
let linking = try xcframeworkMetadataProvider.linking(binaryPath: primaryBinaryPath)
return XCFrameworkNode(path: path,
infoPlist: infoPlist,
primaryBinaryPath: primaryBinaryPath,
linking: linking,
dependencies: [])
}
}

View File

@ -1,26 +0,0 @@
import Basic
import Foundation
public protocol XCFrameworkParsing {
static func parse(path: AbsolutePath, cache: GraphLoaderCaching) throws -> XCFrameworkNode
}
struct XCFrameworkParser: XCFrameworkParsing {
static func parse(path: AbsolutePath, cache: GraphLoaderCaching) throws -> XCFrameworkNode {
if let xcframeworkNode = cache.precompiledNode(path) as? XCFrameworkNode {
return xcframeworkNode
}
let metadataProvider = XCFrameworkMetadataProvider()
let libraries = try metadataProvider.libraries(frameworkPath: path)
let binaryPath = try metadataProvider.binaryPath(frameworkPath: path, libraries: libraries)
let xcframeworkNode = XCFrameworkNode(
path: path,
libraries: libraries,
primaryBinaryPath: binaryPath
)
cache.add(precompiledNode: xcframeworkNode)
return xcframeworkNode
}
}

View File

@ -4,7 +4,17 @@ import Foundation
@testable import TuistCore
public extension FrameworkNode {
static func test(path: AbsolutePath = "/Test.framework") -> FrameworkNode {
FrameworkNode(path: path)
static func test(path: AbsolutePath = "/path/to/Framework.framework",
dsymPath: AbsolutePath? = nil,
bcsymbolmapPaths: [AbsolutePath] = [],
linking: BinaryLinking = .dynamic,
architectures: [BinaryArchitecture] = [],
dependencies: [FrameworkNode] = []) -> FrameworkNode {
FrameworkNode(path: path,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures,
dependencies: dependencies)
}
}

View File

@ -0,0 +1,59 @@
import Basic
import Foundation
import TuistCore
public extension GraphDependencyReference {
static func testFramework(
path: AbsolutePath = "/frameworks/tuist.framework",
binaryPath: AbsolutePath = "/frameworks/tuist.framework/tuist",
isCarthage: Bool = false,
dsymPath: AbsolutePath? = nil,
bcsymbolmapPaths: [AbsolutePath] = [],
linking: BinaryLinking = .dynamic,
architectures: [BinaryArchitecture] = [.arm64],
product: Product = .framework
) -> GraphDependencyReference {
GraphDependencyReference.framework(path: path,
binaryPath: binaryPath,
isCarthage: isCarthage,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures,
product: product)
}
static func testXCFramework(
path: AbsolutePath = "/frameworks/tuist.xcframework",
infoPlist: XCFrameworkInfoPlist = .test(),
primaryBinaryPath: AbsolutePath = "/frameworks/tuist.xcframework/ios-arm64/tuist",
binaryPath: AbsolutePath = "/frameworks/tuist.xcframework/ios-arm64/tuist"
) -> GraphDependencyReference {
GraphDependencyReference.xcframework(path: path,
infoPlist: infoPlist,
primaryBinaryPath: primaryBinaryPath,
binaryPath: binaryPath)
}
static func testLibrary(path: AbsolutePath = "/libraries/library.a",
binaryPath: AbsolutePath = "/libraries/library.a",
linking: BinaryLinking = .static,
architectures: [BinaryArchitecture] = [BinaryArchitecture.arm64],
product: Product = .staticLibrary) -> GraphDependencyReference {
GraphDependencyReference.library(path: path,
binaryPath: binaryPath,
linking: linking,
architectures: architectures,
product: product)
}
static func testSDK(path: AbsolutePath = "/path/CoreData.framework", status: SDKStatus = .required) -> GraphDependencyReference {
GraphDependencyReference.sdk(path: path,
status: status)
}
static func testProduct(target: String = "Target", productName: String = "Target.framework") -> GraphDependencyReference {
GraphDependencyReference.product(target: target,
productName: productName)
}
}

View File

@ -4,8 +4,15 @@ import Foundation
@testable import TuistCore
public extension LibraryNode {
static func test(path: AbsolutePath = "/libTest.a",
publicHeaders: AbsolutePath = "/TestHeaders/") -> LibraryNode {
LibraryNode(path: path, publicHeaders: publicHeaders)
static func test(path: AbsolutePath = "/Library/libTest.a",
publicHeaders: AbsolutePath = "/Library/TestHeaders/",
architectures: [BinaryArchitecture] = [.arm64],
linking: BinaryLinking = .static,
swiftModuleMap: AbsolutePath? = nil) -> LibraryNode {
LibraryNode(path: path,
publicHeaders: publicHeaders,
architectures: architectures,
linking: linking,
swiftModuleMap: swiftModuleMap)
}
}

View File

@ -0,0 +1,20 @@
import Basic
import Foundation
@testable import TuistCore
public extension XCFrameworkInfoPlist {
static func test(libraries: [XCFrameworkInfoPlist.Library] = [.test()]) -> XCFrameworkInfoPlist {
XCFrameworkInfoPlist(libraries: libraries)
}
}
public extension XCFrameworkInfoPlist.Library {
static func test(identifier: String = "test",
path: RelativePath = RelativePath("relative/to/library"),
architectures: [BinaryArchitecture] = [.i386]) -> XCFrameworkInfoPlist.Library {
XCFrameworkInfoPlist.Library(identifier: identifier,
path: path,
architectures: architectures)
}
}

View File

@ -4,18 +4,15 @@ import Foundation
@testable import TuistCore
public extension XCFrameworkNode {
static func test(path: AbsolutePath = "/MyFramework.xcframework") -> XCFrameworkNode {
let libraries: [XCFrameworkInfoPlist.Library] = [
.init(identifier: "ios-x86_64-simulator",
path: RelativePath("MyFramework.framework"), architectures: [.x8664]),
.init(identifier: "ios-arm64",
path: RelativePath("MyFramework.framework"),
architectures: [.arm64]),
]
let primaryBinaryPath = path.appending(RelativePath("ios-arm64/MyFramework.framework/MyFramework"))
return XCFrameworkNode(path: path,
libraries: libraries,
primaryBinaryPath: primaryBinaryPath)
static func test(path: AbsolutePath = "/MyFramework/MyFramework.xcframework",
infoPlist: XCFrameworkInfoPlist = .test(),
primaryBinaryPath: AbsolutePath = "/MyFramework/MyFramework.xcframework/binary",
linking: BinaryLinking = .dynamic,
dependencies: [XCFrameworkNode] = []) -> XCFrameworkNode {
XCFrameworkNode(path: path,
infoPlist: infoPlist,
primaryBinaryPath: primaryBinaryPath,
linking: linking,
dependencies: dependencies)
}
}

View File

@ -0,0 +1,28 @@
import Basic
import Foundation
@testable import TuistCore
public final class MockFrameworkMetadataProvider: MockPrecompiledMetadataProvider, FrameworkMetadataProviding {
public var dsymPathStub: ((AbsolutePath) -> AbsolutePath?)?
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
dsymPathStub?(frameworkPath) ?? nil
}
public var bcsymbolmapPathsStub: ((AbsolutePath) throws -> [AbsolutePath])?
public func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
if let bcsymbolmapPathsStub = bcsymbolmapPathsStub {
return try bcsymbolmapPathsStub(frameworkPath)
} else {
return []
}
}
public var productStub: ((AbsolutePath) throws -> Product)?
public func product(frameworkPath: AbsolutePath) throws -> Product {
if let productStub = productStub {
return try productStub(frameworkPath)
} else {
return .framework
}
}
}

View File

@ -0,0 +1,14 @@
import Basic
import Foundation
@testable import TuistCore
public final class MockLibraryMetadataProvider: MockPrecompiledMetadataProvider, LibraryMetadataProviding {
public var productStub: ((LibraryNode) throws -> Product)?
public func product(library: LibraryNode) throws -> Product {
if let productStub = productStub {
return try productStub(library)
} else {
return .staticLibrary
}
}
}

View File

@ -0,0 +1,34 @@
import Basic
import Foundation
@testable import TuistCore
public class MockPrecompiledMetadataProvider: PrecompiledMetadataProviding {
public init() {}
public var architecturesStub: ((AbsolutePath) throws -> [BinaryArchitecture])?
public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
if let architecturesStub = architecturesStub {
return try architecturesStub(binaryPath)
} else {
return []
}
}
public var linkingStub: ((AbsolutePath) throws -> BinaryLinking)?
public func linking(binaryPath: AbsolutePath) throws -> BinaryLinking {
if let linkingStub = linkingStub {
return try linkingStub(binaryPath)
} else {
return .dynamic
}
}
public var uuidsStub: ((AbsolutePath) throws -> Set<UUID>)?
public func uuids(binaryPath: AbsolutePath) throws -> Set<UUID> {
if let uuidsStub = uuidsStub {
return try uuidsStub(binaryPath)
} else {
return Set()
}
}
}

View File

@ -0,0 +1,23 @@
import Basic
import Foundation
@testable import TuistCore
public final class MockXCFrameworkMetadataProvider: MockPrecompiledMetadataProvider, XCFrameworkMetadataProviding {
public var infoPlistStub: ((AbsolutePath) throws -> XCFrameworkInfoPlist)?
public func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist {
if let infoPlistStub = infoPlistStub {
return try infoPlistStub(xcframeworkPath)
} else {
return XCFrameworkInfoPlist.test()
}
}
public var binaryPathStub: ((AbsolutePath, [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath)?
public func binaryPath(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath {
if let binaryPathStub = binaryPathStub {
return try binaryPathStub(xcframeworkPath, libraries)
} else {
return AbsolutePath.root
}
}
}

View File

@ -0,0 +1,17 @@
import Basic
import Foundation
import TuistCore
public final class MockFrameworkNodeLoader: FrameworkNodeLoading {
public init() {}
var loadStub: ((AbsolutePath) throws -> FrameworkNode)?
public func load(path: AbsolutePath) throws -> FrameworkNode {
if let loadStub = loadStub {
return try loadStub(path)
} else {
return FrameworkNode.test(path: path)
}
}
}

View File

@ -0,0 +1,16 @@
import Basic
import Foundation
import TuistCore
public final class MockLibraryNodeLoader: LibraryNodeLoading {
public init() {}
var loadStub: ((AbsolutePath, AbsolutePath, AbsolutePath?) throws -> LibraryNode)?
public func load(path: AbsolutePath, publicHeaders: AbsolutePath, swiftModuleMap: AbsolutePath?) throws -> LibraryNode {
if let loadStub = loadStub {
return try loadStub(path, publicHeaders, swiftModuleMap)
} else {
return LibraryNode.test(path: path, publicHeaders: publicHeaders, swiftModuleMap: swiftModuleMap)
}
}
}

View File

@ -0,0 +1,16 @@
import Basic
import Foundation
import TuistCore
public final class MockXCFrameworkNodeLoader: XCFrameworkNodeLoading {
public init() {}
var loadStub: ((AbsolutePath) throws -> XCFrameworkNode)?
public func load(path: AbsolutePath) throws -> XCFrameworkNode {
if let loadStub = loadStub {
return try loadStub(path)
} else {
return XCFrameworkNode.test(path: path)
}
}
}

View File

@ -50,7 +50,7 @@ final class BundleCommand: Command {
init(parser: ArgumentParser,
versionsController: VersionsControlling,
installer: Installing) {
let subParser = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview)
_ = parser.add(subparser: BundleCommand.command, overview: BundleCommand.overview)
self.versionsController = versionsController
self.installer = installer
}

View File

@ -180,7 +180,7 @@ final class Installer: Installing {
try FileHandler.shared.delete(installationDirectory)
}
try FileHandler.shared.createFolder(installationDirectory)
let templatesDirectory = temporaryDirectory.path.appending(component: Constants.templatesDirectoryName)
if FileHandler.shared.exists(templatesDirectory) {
try FileHandler.shared.copy(from: templatesDirectory,

View File

@ -165,26 +165,31 @@ final class LinkGenerator: LinkGenerating {
pbxTarget.buildPhases.append(precompiledEmbedPhase)
pbxTarget.buildPhases.append(embedPhase)
var precompiledFrameworkPaths: [AbsolutePath] = []
var frameworkReferences: [GraphDependencyReference] = []
try dependencies.forEach { dependency in
if case let GraphDependencyReference.absolute(path) = dependency {
if path.extension == "xcframework" {
guard let fileRef = fileElements.file(path: path) else {
throw LinkGeneratorError.missingReference(path: path)
}
let buildFile = PBXBuildFile(
file: fileRef,
settings: ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]]
)
pbxproj.add(object: buildFile)
embedPhase.files?.append(buildFile)
} else {
precompiledFrameworkPaths.append(path)
switch dependency {
case .framework:
frameworkReferences.append(dependency)
case .library:
// Do nothing
break
case let .xcframework(metadata):
guard let fileRef = fileElements.file(path: metadata.path) else {
throw LinkGeneratorError.missingReference(path: metadata.path)
}
} else if case let GraphDependencyReference.product(target, _) = dependency {
guard let fileRef = fileElements.product(target: target) else {
throw LinkGeneratorError.missingProduct(name: target)
let buildFile = PBXBuildFile(
file: fileRef,
settings: ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]]
)
pbxproj.add(object: buildFile)
embedPhase.files?.append(buildFile)
case .sdk:
// Do nothing
break
case let .product(metadata):
guard let fileRef = fileElements.product(target: metadata.target) else {
throw LinkGeneratorError.missingProduct(name: metadata.target)
}
let buildFile = PBXBuildFile(file: fileRef,
settings: ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]])
@ -193,10 +198,10 @@ final class LinkGenerator: LinkGenerating {
}
}
if precompiledFrameworkPaths.isEmpty {
if frameworkReferences.isEmpty {
precompiledEmbedPhase.shellScript = "echo \"Skipping, nothing to be embedded.\""
} else {
let script = try embedScriptGenerator.script(sourceRootPath: sourceRootPath, frameworkPaths: precompiledFrameworkPaths)
let script = try embedScriptGenerator.script(sourceRootPath: sourceRootPath, frameworkReferences: frameworkReferences)
precompiledEmbedPhase.shellScript = script.script
precompiledEmbedPhase.inputPaths = script.inputPaths.map(\.pathString)
precompiledEmbedPhase.outputPaths = script.outputPaths
@ -206,11 +211,8 @@ final class LinkGenerator: LinkGenerating {
func setupFrameworkSearchPath(dependencies: [GraphDependencyReference],
pbxTarget: PBXTarget,
sourceRootPath: AbsolutePath) throws {
let paths = dependencies.compactMap { (dependency: GraphDependencyReference) -> AbsolutePath? in
if case let .absolute(path) = dependency { return path }
return nil
}
.map { $0.removingLastComponent() }
let paths = dependencies.compactMap { $0.path }
.map { $0.removingLastComponent() }
let uniquePaths = Array(Set(paths))
try setup(setting: "FRAMEWORK_SEARCH_PATHS",
@ -275,17 +277,25 @@ final class LinkGenerator: LinkGenerating {
pbxproj.add(object: buildPhase)
pbxTarget.buildPhases.append(buildPhase)
func addBuildFile(_ path: AbsolutePath) throws {
guard let fileRef = fileElements.file(path: path) else {
throw LinkGeneratorError.missingReference(path: path)
}
let buildFile = PBXBuildFile(file: fileRef)
pbxproj.add(object: buildFile)
buildPhase.files?.append(buildFile)
}
try dependencies
.sorted()
.forEach { dependency in
switch dependency {
case let .absolute(path):
guard let fileRef = fileElements.file(path: path) else {
throw LinkGeneratorError.missingReference(path: path)
}
let buildFile = PBXBuildFile(file: fileRef)
pbxproj.add(object: buildFile)
buildPhase.files?.append(buildFile)
case let .framework(metadata):
try addBuildFile(metadata.path)
case let .library(metadata):
try addBuildFile(metadata.path)
case let .xcframework(metadata):
try addBuildFile(metadata.path)
case let .product(target, _):
guard let fileRef = fileElements.product(target: target) else {
throw LinkGeneratorError.missingProduct(name: target)

View File

@ -179,15 +179,23 @@ class ProjectFileElements {
sourceRootPath: AbsolutePath,
filesGroup: ProjectGroup) throws {
let sortedDependencies = dependencyReferences.sorted()
func generatePrecompiled(_ path: AbsolutePath) throws {
let fileElement = GroupFileElement(path: path, group: filesGroup)
try generate(fileElement: fileElement,
groups: groups,
pbxproj: pbxproj,
sourceRootPath: sourceRootPath)
}
try sortedDependencies.forEach { dependency in
switch dependency {
case let .absolute(dependencyPath):
let fileElement = GroupFileElement(path: dependencyPath,
group: filesGroup)
try generate(fileElement: fileElement,
groups: groups,
pbxproj: pbxproj,
sourceRootPath: sourceRootPath)
case let .xcframework(metadata):
try generatePrecompiled(metadata.path)
case let .framework(metadata):
try generatePrecompiled(metadata.path)
case let .library(metadata):
try generatePrecompiled(metadata.path)
case let .sdk(sdkNodePath, _):
generateSDKFileElement(sdkNodePath: sdkNodePath,
toGroup: groups.frameworks,

View File

@ -36,30 +36,44 @@ protocol WorkspaceGenerating: AnyObject {
}
final class WorkspaceGenerator: WorkspaceGenerating {
struct Config {
/// The execution context to use when generating
/// descriptors for each project within the workspace / graph
var projectGenerationContext: ExecutionContext
static var `default`: Config {
Config(projectGenerationContext: .concurrent)
}
}
// MARK: - Attributes
private let projectGenerator: ProjectGenerating
private let workspaceStructureGenerator: WorkspaceStructureGenerating
private let schemesGenerator: SchemesGenerating
private let config: Config
// MARK: - Init
convenience init(defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider()) {
convenience init(defaultSettingsProvider: DefaultSettingsProviding = DefaultSettingsProvider(),
config: Config = .default) {
let configGenerator = ConfigGenerator(defaultSettingsProvider: defaultSettingsProvider)
let targetGenerator = TargetGenerator(configGenerator: configGenerator)
let projectGenerator = ProjectGenerator(targetGenerator: targetGenerator,
configGenerator: configGenerator)
self.init(projectGenerator: projectGenerator,
workspaceStructureGenerator: WorkspaceStructureGenerator(),
schemesGenerator: SchemesGenerator())
schemesGenerator: SchemesGenerator(),
config: config)
}
init(projectGenerator: ProjectGenerating,
workspaceStructureGenerator: WorkspaceStructureGenerating,
schemesGenerator: SchemesGenerating) {
schemesGenerator: SchemesGenerating,
config: Config = .default) {
self.projectGenerator = projectGenerator
self.workspaceStructureGenerator = workspaceStructureGenerator
self.schemesGenerator = schemesGenerator
self.config = config
}
// MARK: - WorkspaceGenerating
@ -70,7 +84,7 @@ final class WorkspaceGenerator: WorkspaceGenerating {
logger.notice("Generating workspace \(workspaceName)", metadata: .section)
/// Projects
let projects = try graph.projects.map { project in
let projects = try graph.projects.map(context: config.projectGenerationContext) { project in
try projectGenerator.generate(project: project,
graph: graph,
sourceRootPath: project.path,

View File

@ -11,8 +11,8 @@ protocol EmbedScriptGenerating {
/// It returns the script and the input paths list that should be used to generate a Xcode script phase
/// to embed the given frameworks into the compiled product.
/// - Parameter sourceRootPath: Directory where the Xcode project will be created.
/// - Parameter frameworkPaths: Path to the frameworks.
func script(sourceRootPath: AbsolutePath, frameworkPaths: [AbsolutePath]) throws -> EmbedScript
/// - Parameter frameworkReferences: Framework references.
func script(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript
}
/// It represents a embed frameworks script.
@ -30,17 +30,11 @@ struct EmbedScript {
final class EmbedScriptGenerator: EmbedScriptGenerating {
typealias FrameworkScript = (script: String, inputPaths: [RelativePath], outputPaths: [String])
let frameworkMetadataProvider: FrameworkMetadataProviding
init(frameworkMetadataProvider: FrameworkMetadataProviding = FrameworkMetadataProvider()) {
self.frameworkMetadataProvider = frameworkMetadataProvider
}
func script(sourceRootPath: AbsolutePath, frameworkPaths: [AbsolutePath]) throws -> EmbedScript {
func script(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript {
var script = baseScript()
script.append("\n")
let (frameworksScript, inputPaths, outputPaths) = try self.frameworksScript(sourceRootPath: sourceRootPath, frameworkPaths: frameworkPaths)
let (frameworksScript, inputPaths, outputPaths) = try self.frameworksScript(sourceRootPath: sourceRootPath, frameworkReferences: frameworkReferences)
script.append(frameworksScript)
return EmbedScript(script: script, inputPaths: inputPaths, outputPaths: outputPaths)
@ -48,21 +42,25 @@ final class EmbedScriptGenerator: EmbedScriptGenerating {
// MARK: - Fileprivate
fileprivate func frameworksScript(sourceRootPath: AbsolutePath,
frameworkPaths: [AbsolutePath]) throws -> FrameworkScript {
fileprivate func frameworksScript(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> FrameworkScript {
var script = ""
var inputPaths: [RelativePath] = []
var outputPaths: [String] = []
for frameworkPath in frameworkPaths {
for frameworkReference in frameworkReferences {
guard case let GraphDependencyReference.framework(path, _, _, dsymPath, bcsymbolmapPaths, _, _, _) = frameworkReference else {
preconditionFailure("references need to be of type framework")
break
}
// Framework
let relativeFrameworkPath = frameworkPath.relative(to: sourceRootPath)
let relativeFrameworkPath = path.relative(to: sourceRootPath)
script.append("install_framework \"\(relativeFrameworkPath.pathString)\"\n")
inputPaths.append(relativeFrameworkPath)
outputPaths.append("${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/\(relativeFrameworkPath.basename)")
// .dSYM
if let dsymPath = frameworkMetadataProvider.dsymPath(frameworkPath: frameworkPath) {
if let dsymPath = dsymPath {
let relativeDsymPath = dsymPath.relative(to: sourceRootPath)
script.append("install_dsym \"\(relativeDsymPath.pathString)\"\n")
inputPaths.append(relativeDsymPath)
@ -70,8 +68,7 @@ final class EmbedScriptGenerator: EmbedScriptGenerating {
}
// .bcsymbolmap
let bcsymbolmaps = try frameworkMetadataProvider.bcsymbolmapPaths(frameworkPath: frameworkPath)
for bcsymbolmapPath in bcsymbolmaps {
for bcsymbolmapPath in bcsymbolmapPaths {
let relativeDsymPath = bcsymbolmapPath.relative(to: sourceRootPath)
script.append("install_bcsymbolmap \"\(relativeDsymPath.pathString)\"\n")
inputPaths.append(relativeDsymPath)

View File

@ -11,13 +11,29 @@ public protocol XcodeProjWriting {
// MARK: -
public final class XcodeProjWriter: XcodeProjWriting {
public struct Config {
/// The execution context to use when writing
/// the project descriptors within a workspace descriptor
public var projectDescriptorWritingContext: ExecutionContext
public init(projectDescriptorWritingContext: ExecutionContext) {
self.projectDescriptorWritingContext = projectDescriptorWritingContext
}
public static var `default`: Config {
Config(projectDescriptorWritingContext: .concurrent)
}
}
private let fileHandler: FileHandling
private let system: Systeming
private let config: Config
public init(fileHandler: FileHandling = FileHandler.shared,
system: Systeming = System.shared) {
system: Systeming = System.shared,
config: Config = .default) {
self.fileHandler = fileHandler
self.system = system
self.config = config
}
public func write(project: ProjectDescriptor) throws {
@ -28,7 +44,7 @@ public final class XcodeProjWriter: XcodeProjWriting {
}
public func write(workspace: WorkspaceDescriptor) throws {
try workspace.projectDescriptors.forEach(write)
try workspace.projectDescriptors.forEach(context: config.projectDescriptorWritingContext, write)
try workspace.xcworkspace.write(path: workspace.xcworkspacePath.path, override: true)
try workspace.schemeDescriptors.forEach { try write(scheme: $0, xccontainerPath: workspace.xcworkspacePath) }
try workspace.sideEffectDescriptors.forEach(perform)

View File

@ -4,8 +4,8 @@ import SPMUtility
import TuistCore
import TuistGenerator
import TuistLoader
import TuistSupport
import TuistScaffold
import TuistSupport
private typealias Platform = TuistCore.Platform
private typealias Product = TuistCore.Product
@ -109,7 +109,7 @@ class InitCommand: NSObject, Command {
self.templateGenerator = templateGenerator
self.templateLoader = templateLoader
}
func parse(with parser: ArgumentParser, arguments: [String]) throws -> ArgumentParser.Result {
guard arguments.contains("--template") else { return try parser.parse(arguments) }
// Plucking out path and template argument
@ -117,10 +117,10 @@ class InitCommand: NSObject, Command {
arguments[$0 ..< min($0 + 2, arguments.count)]
}
let filteredArguments = pairedArguments
.filter {
$0.first == "--path" || $0.first == "--template"
}
.flatMap { Array($0) }
.filter {
$0.first == "--path" || $0.first == "--template"
}
.flatMap { Array($0) }
// We want to parse only the name of template, not its arguments which will be dynamically added
let resultArguments = try parser.parse(filteredArguments)
@ -150,7 +150,7 @@ class InitCommand: NSObject, Command {
let path = self.path(arguments: arguments)
let name = try self.name(arguments: arguments, path: path)
try verifyDirectoryIsEmpty(path: path)
let directories = try templatesDirectoryLocator.templateDirectories(at: path)
if let template = arguments.get(templateArgument) {
guard
@ -174,7 +174,6 @@ class InitCommand: NSObject, Command {
attributes: ["name": name, "platform": platform.caseValue])
}
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
}
@ -189,7 +188,7 @@ class InitCommand: NSObject, Command {
throw InitCommandError.nonEmptyDirectory(path)
}
}
/// Validates if all `attributes` from `template` have been provided
/// If those attributes are optional, they default to `default` if not provided
/// - Returns: Array of parsed attributes
@ -218,7 +217,7 @@ class InitCommand: NSObject, Command {
return mutableDict
}
}
/// Finds template directory
/// - Parameters:
/// - templateDirectories: Paths of available templates

View File

@ -22,7 +22,7 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
}
// MARK: - TemplatesDirectoryLocating
public func locateTuistTemplates() -> AbsolutePath? {
#if DEBUG
// Used only for debug purposed to find templates in your tuist working directory
@ -44,7 +44,6 @@ public final class TemplatesDirectoryLocator: TemplatesDirectoryLocating {
return candidates.first(where: FileHandler.shared.exists)
}
public func locateUserTemplates(at: AbsolutePath) -> AbsolutePath? {
guard let customTemplatesDirectory = locate(from: at) else { return nil }
if !FileHandler.shared.exists(customTemplatesDirectory) { return nil }

View File

@ -14,7 +14,7 @@ public final class MockTemplatesDirectoryLocator: TemplatesDirectoryLocating {
public func templateDirectories(at path: AbsolutePath) throws -> [AbsolutePath] {
try templateDirectoriesStub?(path) ?? []
}
public func locateTuistTemplates() -> AbsolutePath? {
locateTuistTemplatesStub?()
}

View File

@ -3,7 +3,7 @@ import Foundation
/// Ensures that writing and reading from property annotated with this property wrapper is thread safe
/// Taken from https://www.onswiftwings.com/posts/atomic-property-wrapper/
@propertyWrapper
struct Atomic<Value> {
class Atomic<Value> {
private var value: Value
private let lock = NSLock()
@ -16,13 +16,13 @@ struct Atomic<Value> {
set { store(newValue: newValue) }
}
func load() -> Value {
private func load() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
mutating func store(newValue: Value) {
private func store(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue

View File

@ -0,0 +1,117 @@
import Foundation
/// Execution Context
///
/// Defines a context for operations to be performed in.
/// e.g. `.concurrent` or `.serial`
///
public struct ExecutionContext {
public enum ExecutionType {
case serial
case concurrent
}
public var executionType: ExecutionType
public init(executionType: ExecutionType) {
self.executionType = executionType
}
public static var serial: ExecutionContext {
ExecutionContext(executionType: .serial)
}
public static var concurrent: ExecutionContext {
ExecutionContext(executionType: .concurrent)
}
}
public extension Array {
/// Map (with execution context)
///
/// - Parameters:
/// - context: The execution context to perform the `transform` with
/// - transform: The transformation closure to apply to the array
func map<B>(context: ExecutionContext, _ transform: (Element) throws -> B) rethrows -> [B] {
switch context.executionType {
case .serial:
return try map(transform)
case .concurrent:
return try concurrentMap(transform)
}
}
/// For Each (with execution context)
///
/// - Parameters:
/// - context: The execution context to perform the `perform` operation with
/// - transform: The perform closure to call on each element in the array
func forEach(context: ExecutionContext, _ perform: (Element) throws -> Void) rethrows {
switch context.executionType {
case .serial:
return try forEach(perform)
case .concurrent:
return try concurrentForEach(perform)
}
}
}
// MARK: - Private
//
// Concurrent Map / For Each
// based on https://talk.objc.io/episodes/S01E90-concurrent-map
//
extension Array {
private final class ThreadSafe<A> {
private var _value: A
private let queue = DispatchQueue(label: "ThreadSafe")
init(_ value: A) {
_value = value
}
var value: A {
queue.sync { _value }
}
func atomically(_ transform: @escaping (inout A) -> Void) {
queue.async {
transform(&self._value)
}
}
}
private func concurrentMap<B>(_ transform: (Element) throws -> B) rethrows -> [B] {
let result = ThreadSafe([Result<B, Error>?](repeating: nil, count: count))
DispatchQueue.concurrentPerform(iterations: count) { idx in
let element = self[idx]
do {
let transformed = try transform(element)
result.atomically {
$0[idx] = .success(transformed)
}
} catch {
result.atomically {
$0[idx] = .failure(error)
}
}
}
return try result.value.map { try $0!.get() }
}
private func concurrentForEach(_ perform: (Element) throws -> Void) rethrows {
let result = ThreadSafe([Error?](repeating: nil, count: count))
DispatchQueue.concurrentPerform(iterations: count) { idx in
let element = self[idx]
do {
try perform(element)
} catch {
result.atomically {
$0[idx] = error
}
}
}
return try result.value.compactMap { $0 }.forEach {
throw $0
}
}
}

View File

@ -65,9 +65,7 @@ public struct StandardLogHandler: LogHandler {
extension FileHandle {
func print(_ string: String, terminator: String = "\n") {
string.data(using: .utf8)
.map(write)
terminator.data(using: .utf8)
(string + terminator).data(using: .utf8)
.map(write)
}
}

View File

@ -32,14 +32,18 @@ public class XcodeController: XcodeControlling {
/// - Throws: An error if it can't be obtained.
public func selected() throws -> Xcode? {
// Return cached value if available
guard selectedXcode == nil else { return selectedXcode }
if let cached = selectedXcode {
return cached
}
// e.g. /Applications/Xcode.app/Contents/Developer
guard let path = try? System.shared.capture(["xcode-select", "-p"]).spm_chomp() else {
return nil
}
selectedXcode = try Xcode.read(path: AbsolutePath(path).parentDirectory.parentDirectory)
return selectedXcode
let xcode = try Xcode.read(path: AbsolutePath(path).parentDirectory.parentDirectory)
selectedXcode = xcode
return xcode
}
enum XcodeVersionError: FatalError {

View File

@ -9,7 +9,14 @@ import Foundation
import TuistSupport
public struct TestingLogHandler: LogHandler {
static var collected: [Logger.Level: [String]] = [:]
static var collected: [Logger.Level: [String]] {
collectionQueue.sync {
collectedLogs
}
}
private static var collectionQueue = DispatchQueue(label: "io.tuist.tuistTestingSupport.logging")
private static var collectedLogs: [Logger.Level: [String]] = [:]
public var logLevel: Logger.Level
public let label: String
@ -25,7 +32,9 @@ public struct TestingLogHandler: LogHandler {
metadata _: Logger.Metadata?,
file _: String, function _: String, line _: UInt
) {
Self.collected[level, default: []].append(message.description)
TestingLogHandler.collectionQueue.async {
TestingLogHandler.collectedLogs[level, default: []].append(message.description)
}
}
public var metadata = Logger.Metadata()

View File

@ -23,10 +23,9 @@ final class FrameworkMetadataProviderIntegrationTests: TuistTestCase {
// Given
let carthagePath = try temporaryFixture("Carthage/")
let frameworkPath = FileHandler.shared.glob(carthagePath, glob: "*.framework").first!
let framework = FrameworkNode(path: frameworkPath)
// When
let got = try subject.bcsymbolmapPaths(framework: framework).sorted()
let got = try subject.bcsymbolmapPaths(frameworkPath: frameworkPath).sorted()
// Then
XCTAssertEqual(got, [
@ -39,10 +38,9 @@ final class FrameworkMetadataProviderIntegrationTests: TuistTestCase {
// Given
let carthagePath = try temporaryFixture("Carthage/")
let frameworkPath = FileHandler.shared.glob(carthagePath, glob: "*.framework").first!
let framework = FrameworkNode(path: frameworkPath)
// When
let got = subject.dsymPath(framework: framework)
let got = subject.dsymPath(frameworkPath: frameworkPath)
// Then
XCTAssertTrue(got == carthagePath.appending(component: "\(frameworkPath.basename).dSYM"))

View File

@ -22,10 +22,9 @@ final class PrecompiledMetadataProviderIntegrationTests: TuistTestCase {
func test_architectures() throws {
// Given
let frameworkPath = try temporaryFixture("xpm.framework")
let framework = FrameworkNode(path: frameworkPath)
// When
let got = try subject.architectures(precompiled: framework)
let got = try subject.architectures(binaryPath: FrameworkNode.binaryPath(frameworkPath: frameworkPath))
// Then
XCTAssertEqual(got.map(\.rawValue).sorted(), ["arm64", "x86_64"])
@ -34,10 +33,9 @@ final class PrecompiledMetadataProviderIntegrationTests: TuistTestCase {
func test_uuids() throws {
// Given
let frameworkPath = try temporaryFixture("xpm.framework")
let framework = FrameworkNode(path: frameworkPath)
// When
let got = try subject.uuids(precompiled: framework)
let got = try subject.uuids(binaryPath: FrameworkNode.binaryPath(frameworkPath: frameworkPath))
// Then
let expected = Set([

View File

@ -8,57 +8,27 @@ import XCTest
@testable import TuistSupportTesting
final class GraphDependencyReferenceTests: TuistUnitTestCase {
func test_equal() {
let subjects: [(GraphDependencyReference, GraphDependencyReference, Bool)] = [
// Absolute
(.absolute(.init("/a.framework")), .absolute(.init("/a.framework")), true),
(.absolute(.init("/a.framework")), .product(target: "Main", productName: "Main.app"), false),
(.absolute(.init("/a.framework")), .sdk(.init("/CoreData.framework"), .required), false),
// Product
(.product(target: "Main", productName: "Main.app"), .product(target: "Main", productName: "Main.app"), true),
(.product(target: "Main", productName: "Main.app"), .absolute(.init("/a.framework")), false),
(.product(target: "Main", productName: "Main.app"), .sdk(.init("/CoreData.framework"), .required), false),
(.product(target: "Main-iOS", productName: "Main.app"), .product(target: "Main-macOS", productName: "Main.app"), false),
// SDK
(.sdk(.init("/CoreData.framework"), .required), .sdk(.init("/CoreData.framework"), .required), true),
(.sdk(.init("/CoreData.framework"), .required), .product(target: "Main", productName: "Main.app"), false),
(.sdk(.init("/CoreData.framework"), .required), .absolute(.init("/a.framework")), false),
]
XCTAssertEqualPairs(subjects)
}
func test_compare() {
XCTAssertFalse(GraphDependencyReference.absolute("/A") < .absolute("/A"))
XCTAssertTrue(GraphDependencyReference.absolute("/A") < .absolute("/B"))
XCTAssertFalse(GraphDependencyReference.absolute("/B") < .absolute("/A"))
// Given
let subject = makeReferences().sorted()
XCTAssertFalse(GraphDependencyReference.product(target: "A", productName: "A.framework") < .product(target: "A", productName: "A.framework"))
XCTAssertTrue(GraphDependencyReference.product(target: "A", productName: "A.framework") < .product(target: "B", productName: "B.framework"))
XCTAssertFalse(GraphDependencyReference.product(target: "B", productName: "B.framework") < .product(target: "A", productName: "A.framework"))
XCTAssertTrue(GraphDependencyReference.product(target: "A", productName: "A.app") < .product(target: "A", productName: "A.framework"))
XCTAssertTrue(GraphDependencyReference.product(target: "/A", productName: "A.framework") < .absolute("/A"))
XCTAssertTrue(GraphDependencyReference.product(target: "/A", productName: "A.framework") < .absolute("/B"))
XCTAssertTrue(GraphDependencyReference.product(target: "/B", productName: "B.framework") < .absolute("/A"))
XCTAssertFalse(GraphDependencyReference.absolute("/A") < .product(target: "/A", productName: "A.framework"))
XCTAssertFalse(GraphDependencyReference.absolute("/A") < .product(target: "/B", productName: "B.framework"))
XCTAssertFalse(GraphDependencyReference.absolute("/B") < .product(target: "/A", productName: "A.framework"))
XCTAssertEqual(subject, [
.sdk(path: "/A.framework", status: .required),
.sdk(path: "/B.framework", status: .optional),
.product(target: "A", productName: "A.framework"),
.product(target: "B", productName: "B.framework"),
.testLibrary(path: "/libraries/A.library"),
.testLibrary(path: "/libraries/B.library"),
.testFramework(path: "/frameworks/A.framework"),
.testFramework(path: "/frameworks/B.framework"),
.testXCFramework(path: "/xcframeworks/A.xcframework"),
.testXCFramework(path: "/xcframeworks/B.xcframework"),
])
}
func test_compare_isStable() {
// Given
let subject: [GraphDependencyReference] = [
.absolute("/A"),
.absolute("/B"),
.product(target: "A", productName: "A.framework"),
.product(target: "B", productName: "B.framework"),
.sdk("/A.framework", .required),
.sdk("/B.framework", .optional),
]
let subject = makeReferences()
// When
let sorted = (0 ..< 10).map { _ in subject.shuffled().sorted() }
@ -67,4 +37,19 @@ final class GraphDependencyReferenceTests: TuistUnitTestCase {
let unstable = sorted.dropFirst().filter { $0 != sorted.first }
XCTAssertTrue(unstable.isEmpty)
}
func makeReferences() -> [GraphDependencyReference] {
[
.testXCFramework(path: "/xcframeworks/A.xcframework"),
.testXCFramework(path: "/xcframeworks/B.xcframework"),
.testFramework(path: "/frameworks/A.framework"),
.testFramework(path: "/frameworks/B.framework"),
.testLibrary(path: "/libraries/A.library"),
.testLibrary(path: "/libraries/B.library"),
.product(target: "A", productName: "A.framework"),
.product(target: "B", productName: "B.framework"),
.sdk(path: "/A.framework", status: .required),
.sdk(path: "/B.framework", status: .optional),
]
}
}

View File

@ -4,6 +4,7 @@ import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
@testable import TuistSupport
@testable import TuistSupportTesting
@ -22,7 +23,7 @@ final class GraphErrorTests: XCTestCase {
final class GraphTests: TuistUnitTestCase {
func test_frameworks() throws {
let framework = FrameworkNode(path: AbsolutePath("/path/to/framework.framework"))
let framework = FrameworkNode.test(path: AbsolutePath("/path/to/framework.framework"))
let cache = GraphLoaderCache()
cache.add(precompiledNode: framework)
let graph = Graph.test(cache: cache)
@ -78,7 +79,7 @@ final class GraphTests: TuistUnitTestCase {
func test_linkableDependencies_whenPrecompiled() throws {
let target = Target.test(name: "Main")
let precompiledNode = FrameworkNode(path: AbsolutePath("/test/test.framework"))
let precompiledNode = FrameworkNode.test(path: AbsolutePath("/test/test.framework"))
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
@ -91,7 +92,7 @@ final class GraphTests: TuistUnitTestCase {
output: "Architectures in the fat file: Alamofire are: x86_64 arm64")
let got = try graph.linkableDependencies(path: project.path, name: target.name)
XCTAssertEqual(got.first, .absolute(precompiledNode.path))
XCTAssertEqual(got.first, GraphDependencyReference(precompiledNode: precompiledNode))
}
func test_linkableDependencies_whenALibraryTarget() throws {
@ -506,8 +507,8 @@ final class GraphTests: TuistUnitTestCase {
func test_librariesPublicHeaders() throws {
let target = Target.test(name: "Main")
let publicHeadersPath = AbsolutePath("/test/public/")
let precompiledNode = LibraryNode(path: AbsolutePath("/test/test.a"),
publicHeaders: publicHeadersPath)
let precompiledNode = LibraryNode.test(path: AbsolutePath("/test/test.a"),
publicHeaders: publicHeadersPath)
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
@ -564,7 +565,7 @@ final class GraphTests: TuistUnitTestCase {
func test_embeddableFrameworks_when_dependencyIsAFramework() throws {
let frameworkPath = AbsolutePath("/test/test.framework")
let target = Target.test(name: "Main", platform: .iOS)
let frameworkNode = FrameworkNode(path: frameworkPath)
let frameworkNode = FrameworkNode.test(path: frameworkPath)
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
@ -578,7 +579,7 @@ final class GraphTests: TuistUnitTestCase {
let got = try graph.embeddableFrameworks(path: project.path, name: target.name)
XCTAssertEqual(got.first, GraphDependencyReference.absolute(frameworkPath))
XCTAssertEqual(got.first, GraphDependencyReference(precompiledNode: frameworkNode))
}
func test_embeddableFrameworks_when_dependencyIsATransitiveFramework() throws {
@ -587,7 +588,7 @@ final class GraphTests: TuistUnitTestCase {
let project = Project.test(targets: [target])
let frameworkPath = AbsolutePath("/test/test.framework")
let frameworkNode = FrameworkNode(path: frameworkPath)
let frameworkNode = FrameworkNode.test(path: frameworkPath)
let dependencyNode = TargetNode(
project: project,
@ -610,7 +611,7 @@ final class GraphTests: TuistUnitTestCase {
XCTAssertEqual(got, [
GraphDependencyReference.product(target: "Dependency", productName: "Dependency.framework"),
GraphDependencyReference.absolute(frameworkPath),
GraphDependencyReference(precompiledNode: frameworkNode),
])
}
@ -619,7 +620,7 @@ final class GraphTests: TuistUnitTestCase {
let target = Target.test(name: "Main")
let project = Project.test(targets: [target])
let frameworkNode = FrameworkNode(path: "/test/StaticFramework.framework")
let frameworkNode = FrameworkNode.test(path: "/test/StaticFramework.framework", linking: .static)
let targetNode = TargetNode(
project: project,
target: target,
@ -629,9 +630,6 @@ final class GraphTests: TuistUnitTestCase {
cache.add(targetNode: targetNode)
let graph = Graph.test(cache: cache)
system.succeedCommand("/usr/bin/file", "/test/StaticFramework.framework/StaticFramework",
output: "current ar archive random library")
// When
let result = try graph.embeddableFrameworks(path: project.path, name: target.name)
@ -769,8 +767,7 @@ final class GraphTests: TuistUnitTestCase {
func test_librariesSearchPaths() throws {
// Given
let target = Target.test(name: "Main")
let precompiledNode = LibraryNode(path: AbsolutePath("/test/test.a"),
publicHeaders: AbsolutePath("/test/public/"))
let precompiledNode = LibraryNode.test(path: "/test/test.a", publicHeaders: "/test/public/")
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
@ -790,12 +787,8 @@ final class GraphTests: TuistUnitTestCase {
func test_librariesSwiftIncludePaths() throws {
// Given
let target = Target.test(name: "Main")
let precompiledNodeA = LibraryNode(path: AbsolutePath("/test/test.a"),
publicHeaders: AbsolutePath("/test/public/"),
swiftModuleMap: AbsolutePath("/test/modules/test.swiftmodulemap"))
let precompiledNodeB = LibraryNode(path: AbsolutePath("/test/another.a"),
publicHeaders: AbsolutePath("/test/public/"),
swiftModuleMap: nil)
let precompiledNodeA = LibraryNode.test(path: "/test/test.a", swiftModuleMap: "/test/modules/test.swiftmodulemap")
let precompiledNodeB = LibraryNode.test(path: "/test/another.a", swiftModuleMap: nil)
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
@ -959,9 +952,9 @@ final class GraphTests: TuistUnitTestCase {
System.shared = System()
let cache = GraphLoaderCache()
let graph = Graph.test(cache: cache)
let framework = FrameworkNode(path: fixturePath(path: RelativePath("xpm.framework")))
let library = LibraryNode(path: fixturePath(path: RelativePath("libStaticLibrary.a")),
publicHeaders: fixturePath(path: RelativePath("")))
let framework = FrameworkNode.test(path: fixturePath(path: RelativePath("xpm.framework")), architectures: [.x8664, .arm64])
let library = LibraryNode.test(path: fixturePath(path: RelativePath("libStaticLibrary.a")),
publicHeaders: fixturePath(path: RelativePath("")))
let target = TargetNode.test(dependencies: [framework, library])
cache.add(targetNode: target)
cache.add(precompiledNode: framework)
@ -969,36 +962,36 @@ final class GraphTests: TuistUnitTestCase {
let expected = """
[
{
"product" : "\(target.target.product.rawValue)",
"bundle_id" : "\(target.target.bundleId)",
"platform" : "\(target.target.platform.rawValue)",
"path" : "\(target.path)",
"dependencies" : [
{
"product" : "\(target.target.product.rawValue)",
"bundle_id" : "\(target.target.bundleId)",
"platform" : "\(target.target.platform.rawValue)",
"path" : "\(target.path)",
"dependencies" : [
"xpm",
"libStaticLibrary"
],
"name" : "Target",
"type" : "source"
},
{
"path" : "\(library.path)",
"architectures" : [
"x86_64"
],
"product" : "static_library",
"name" : "\(library.name)",
"type" : "precompiled"
},
{
"path" : "\(framework.path)",
"architectures" : [
"x86_64",
],
"name" : "Target",
"type" : "source"
},
{
"path" : "\(library.path)",
"architectures" : [
"arm64"
],
"product" : "framework",
"name" : "\(framework.name)",
"type" : "precompiled"
],
"product" : "static_library",
"name" : "\(library.name)",
"type" : "precompiled"
},
{
"path" : "\(framework.path)",
"architectures" : [
"x86_64",
"arm64"
],
"product" : "framework",
"name" : "\(framework.name)",
"type" : "precompiled"
}
]
"""
@ -1010,7 +1003,7 @@ final class GraphTests: TuistUnitTestCase {
// MARK: - Helpers
private func mockDynamicFrameworkNode(at path: AbsolutePath) -> FrameworkNode {
let precompiledNode = FrameworkNode(path: path)
let precompiledNode = FrameworkNode.test()
let binaryPath = path.appending(component: path.basenameWithoutExt)
system.succeedCommand("/usr/bin/file",
binaryPath.pathString,

View File

@ -8,16 +8,26 @@ import XCTest
final class FrameworkNodeTests: TuistUnitTestCase {
var subject: FrameworkNode!
var path: AbsolutePath!
var frameworkPath: AbsolutePath!
var dsymPath: AbsolutePath?
var bcsymbolmapPaths: [AbsolutePath]!
override func setUp() {
super.setUp()
path = AbsolutePath("/test.framework")
subject = FrameworkNode(path: path)
let path = AbsolutePath.root
frameworkPath = path.appending(component: "test.framework")
dsymPath = path.appending(component: "test.dSYM")
bcsymbolmapPaths = [path.appending(component: "test.bcsymbolmap")]
subject = FrameworkNode(path: frameworkPath,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: .dynamic)
}
override func tearDown() {
path = nil
frameworkPath = nil
dsymPath = nil
bcsymbolmapPaths = nil
subject = nil
super.tearDown()
}
@ -32,25 +42,49 @@ final class FrameworkNodeTests: TuistUnitTestCase {
func test_isCarthage() {
XCTAssertFalse(subject.isCarthage)
subject = FrameworkNode(path: AbsolutePath("/path/Carthage/Build/iOS/A.framework"))
subject = FrameworkNode(path: AbsolutePath("/path/Carthage/Build/iOS/A.framework"),
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: .dynamic)
XCTAssertTrue(subject.isCarthage)
}
func test_encode() {
// Given
System.shared = System()
let framework = FrameworkNode(path: fixturePath(path: RelativePath("xpm.framework")))
let expected = """
{
"path": "\(framework.path)",
"architectures": ["x86_64", "arm64"],
"name": "xpm",
"path": "\(subject.path)",
"architectures": [],
"name": "test",
"type": "precompiled",
"product": "framework"
}
"""
// Then
XCTAssertEncodableEqualToJson(framework, expected)
XCTAssertEncodableEqualToJson(subject, expected)
}
func test_product_when_static() {
// Given
let subject = FrameworkNode.test(linking: .static)
// When
let got = subject.product
// Then
XCTAssertEqual(got, .staticFramework)
}
func test_product_when_dynamic() {
// Given
let subject = FrameworkNode.test(linking: .dynamic)
// When
let got = subject.product
// Then
XCTAssertEqual(got, .framework)
}
}

View File

@ -16,8 +16,8 @@ final class GraphNodeTests: XCTestCase {
let c2 = TargetNode(project: .test(path: "/path/c"),
target: .test(name: "c2"),
dependencies: [])
let d = LibraryNode(path: "/path/a", publicHeaders: "/path/to/headers")
let e = LibraryNode(path: "/path/c", publicHeaders: "/path/to/headers")
let d = LibraryNode(path: "/path/a", publicHeaders: "/path/to/headers", architectures: [.arm64], linking: .static)
let e = LibraryNode(path: "/path/c", publicHeaders: "/path/to/headers", architectures: [.arm64], linking: .static)
// When
var set = Set<GraphNode>()
@ -48,7 +48,7 @@ final class GraphNodeTests: XCTestCase {
// Given
let a = GraphNode(path: "/a", name: "a")
let b = TargetNode(project: .test(path: "/a"), target: .test(), dependencies: [])
let c = LibraryNode(path: "/a", publicHeaders: "/path/to/headers")
let c = LibraryNode(path: "/a", publicHeaders: "/path/to/headers", architectures: [.arm64], linking: .static)
// When / Then
let all = [a, b, c]

View File

@ -13,7 +13,7 @@ final class LibraryNodeTests: TuistUnitTestCase {
override func setUp() {
super.setUp()
path = AbsolutePath("/test.a")
subject = LibraryNode(path: path, publicHeaders: AbsolutePath("/headers"))
subject = LibraryNode(path: path, publicHeaders: AbsolutePath("/headers"), architectures: [.arm64], linking: .static)
}
override func tearDown() {
@ -32,9 +32,21 @@ final class LibraryNodeTests: TuistUnitTestCase {
func test_equality() {
// Given
let a1 = LibraryNode(path: "/a", publicHeaders: "/a/header", swiftModuleMap: "/a/swiftmodulemap")
let a2 = LibraryNode(path: "/a", publicHeaders: "/a/header/2", swiftModuleMap: "/a/swiftmodulemap")
let b = LibraryNode(path: "/b", publicHeaders: "/b/header", swiftModuleMap: "/b/swiftmodulemap")
let a1 = LibraryNode(path: "/a",
publicHeaders: "/a/header",
architectures: [.arm64],
linking: .static,
swiftModuleMap: "/a/swiftmodulemap")
let a2 = LibraryNode(path: "/a",
publicHeaders: "/a/header/2",
architectures: [.arm64],
linking: .static,
swiftModuleMap: "/a/swiftmodulemap")
let b = LibraryNode(path: "/b",
publicHeaders: "/b/header",
architectures: [.arm64],
linking: .static,
swiftModuleMap: "/b/swiftmodulemap")
// When / Then
XCTAssertEqual(a1, a1)
@ -47,13 +59,15 @@ final class LibraryNodeTests: TuistUnitTestCase {
// Given
System.shared = System()
let library = LibraryNode(path: fixturePath(path: RelativePath("libStaticLibrary.a")),
publicHeaders: fixturePath(path: RelativePath("")))
publicHeaders: fixturePath(path: RelativePath("")),
architectures: [.arm64],
linking: .static)
let expected = """
{
"type": "precompiled",
"path" : "\(library.path)",
"path" : "\(library.path.pathString)",
"architectures" : [
"x86_64"
"arm64"
],
"name" : "\(library.name)",
"product" : "static_library"
@ -63,4 +77,26 @@ final class LibraryNodeTests: TuistUnitTestCase {
// Then
XCTAssertEncodableEqualToJson(library, expected)
}
func test_product_when_static() {
// Given
let subject = LibraryNode.test(linking: .static)
// When
let got = subject.product
// Then
XCTAssertEqual(got, .staticLibrary)
}
func test_product_when_dynamic() {
// Given
let subject = LibraryNode.test(linking: .dynamic)
// When
let got = subject.product
// Then
XCTAssertEqual(got, .dynamicLibrary)
}
}

View File

@ -27,27 +27,22 @@ final class XCFrameworkNodeTests: TuistUnitTestCase {
let expected = """
{
"path" : "\\/MyFramework.xcframework",
"libraries" : [
{
"SupportedArchitectures" : [
"x86_64"
],
"LibraryIdentifier" : "ios-x86_64-simulator",
"LibraryPath" : "MyFramework.framework"
},
{
"SupportedArchitectures" : [
"arm64"
],
"LibraryIdentifier" : "ios-arm64",
"LibraryPath" : "MyFramework.framework"
}
],
"path" : "/MyFramework/MyFramework.xcframework",
"name" : "MyFramework",
"type" : "precompiled"
"type" : "xcframework",
"linking": "dynamic",
"info_plist" : {
"AvailableLibraries" : [
{
"SupportedArchitectures" : [
"i386"
],
"LibraryIdentifier" : "test",
"LibraryPath" : "relative/to/library"
}
]
}
}
"""
// Then

View File

@ -6,15 +6,6 @@ import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class BinaryArchitectureTests: TuistTestCase {
func test_rawValue() {
XCTAssertEqual(BinaryArchitecture.x8664.rawValue, "x86_64")
XCTAssertEqual(BinaryArchitecture.i386.rawValue, "i386")
XCTAssertEqual(BinaryArchitecture.armv7.rawValue, "armv7")
XCTAssertEqual(BinaryArchitecture.armv7s.rawValue, "armv7s")
}
}
final class PrecompiledMetadataProviderTests: TuistUnitTestCase {
var subject: PrecompiledMetadataProvider!

View File

@ -20,10 +20,10 @@ final class XCFrameworkMetadataProviderTests: XCTestCase {
func test_libraries_when_frameworkIsPresent() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyFramework.xcframework"))
let libraries = try subject.libraries(frameworkPath: frameworkPath)
let infoPlist = try subject.infoPlist(xcframeworkPath: frameworkPath)
// Then
XCTAssertEqual(libraries, [
XCTAssertEqual(infoPlist.libraries, [
.init(identifier: "ios-x86_64-simulator",
path: RelativePath("MyFramework.framework"),
architectures: [.x8664]),
@ -36,8 +36,8 @@ final class XCFrameworkMetadataProviderTests: XCTestCase {
func test_binaryPath_when_frameworkIsPresent() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyFramework.xcframework"))
let libraries = try subject.libraries(frameworkPath: frameworkPath)
let binaryPath = try subject.binaryPath(frameworkPath: frameworkPath, libraries: libraries)
let infoPlist = try subject.infoPlist(xcframeworkPath: frameworkPath)
let binaryPath = try subject.binaryPath(xcframeworkPath: frameworkPath, libraries: infoPlist.libraries)
// Then
XCTAssertEqual(
@ -49,10 +49,10 @@ final class XCFrameworkMetadataProviderTests: XCTestCase {
func test_libraries_when_staticLibraryIsPresent() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyStaticLibrary.xcframework"))
let libraries = try subject.libraries(frameworkPath: frameworkPath)
let infoPlist = try subject.infoPlist(xcframeworkPath: frameworkPath)
// Then
XCTAssertEqual(libraries, [
XCTAssertEqual(infoPlist.libraries, [
.init(identifier: "ios-x86_64-simulator",
path: RelativePath("libMyStaticLibrary.a"),
architectures: [.x8664]),
@ -65,8 +65,8 @@ final class XCFrameworkMetadataProviderTests: XCTestCase {
func test_binaryPath_when_staticLibraryIsPresent() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyStaticLibrary.xcframework"))
let libraries = try subject.libraries(frameworkPath: frameworkPath)
let binaryPath = try subject.binaryPath(frameworkPath: frameworkPath, libraries: libraries)
let infoPlist = try subject.infoPlist(xcframeworkPath: frameworkPath)
let binaryPath = try subject.binaryPath(xcframeworkPath: frameworkPath, libraries: infoPlist.libraries)
// Then
XCTAssertEqual(

View File

@ -0,0 +1,16 @@
import Basic
import Foundation
import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class BinaryArchitectureTests: TuistTestCase {
func test_rawValue() {
XCTAssertEqual(BinaryArchitecture.x8664.rawValue, "x86_64")
XCTAssertEqual(BinaryArchitecture.i386.rawValue, "i386")
XCTAssertEqual(BinaryArchitecture.armv7.rawValue, "armv7")
XCTAssertEqual(BinaryArchitecture.armv7s.rawValue, "armv7s")
}
}

View File

@ -0,0 +1,99 @@
import Basic
import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
@testable import TuistSupportTesting
final class FrameworkNodeLoaderErrorTests: TuistUnitTestCase {
func test_type_when_frameworkNotFound() {
// Given
let path = AbsolutePath("/frameworks/tuist.framework")
let subject = FrameworkNodeLoaderError.frameworkNotFound(path)
// When
let got = subject.type
// Then
XCTAssertEqual(got, .abort)
}
func test_description_when_frameworkNotFound() {
// Given
let path = AbsolutePath("/frameworks/tuist.framework")
let subject = FrameworkNodeLoaderError.frameworkNotFound(path)
// When
let got = subject.description
// Then
XCTAssertEqual(got, "Couldn't find framework at \(path.pathString)")
}
}
final class FrameworkNodeLoaderTests: TuistUnitTestCase {
var frameworkMetadataProvider: MockFrameworkMetadataProvider!
var subject: FrameworkNodeLoader!
override func setUp() {
frameworkMetadataProvider = MockFrameworkMetadataProvider()
subject = FrameworkNodeLoader(frameworkMetadataProvider: frameworkMetadataProvider)
super.setUp()
}
override func tearDown() {
frameworkMetadataProvider = nil
subject = nil
super.tearDown()
}
func test_load_when_the_framework_doesnt_exist() throws {
// Given
let path = try temporaryPath()
let frameworkPath = path.appending(component: "tuist.framework")
// Then
XCTAssertThrowsSpecific(try subject.load(path: frameworkPath), FrameworkNodeLoaderError.frameworkNotFound(frameworkPath))
}
func test_oad_when_the_framework_exists() throws {
// Given
let path = try temporaryPath()
let frameworkPath = path.appending(component: "tuist.framework")
let dsymPath = path.appending(component: "tuist.dSYM")
let bcsymbolmapPaths = [path.appending(component: "tuist.bcsymbolmap")]
let architectures = [BinaryArchitecture.armv7s]
let linking = BinaryLinking.dynamic
try FileHandler.shared.touch(frameworkPath)
frameworkMetadataProvider.dsymPathStub = { path in
XCTAssertEqual(path, frameworkPath)
return dsymPath
}
frameworkMetadataProvider.bcsymbolmapPathsStub = { path in
XCTAssertEqual(path, frameworkPath)
return bcsymbolmapPaths
}
frameworkMetadataProvider.linkingStub = { path in
XCTAssertEqual(path, FrameworkNode.binaryPath(frameworkPath: frameworkPath))
return linking
}
frameworkMetadataProvider.architecturesStub = { path in
XCTAssertEqual(path, FrameworkNode.binaryPath(frameworkPath: frameworkPath))
return architectures
}
// When
let got = try subject.load(path: frameworkPath)
// Then
XCTAssertEqual(got, FrameworkNode(path: frameworkPath,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures,
dependencies: []))
}
}

View File

@ -0,0 +1,176 @@
import Basic
import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
@testable import TuistSupportTesting
final class LibraryNodeLoaderErrorTests: TuistUnitTestCase {
func test_type_when_libraryNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.libraryNotFound(path)
// When
let got = subject.type
// Then
XCTAssertEqual(got, .abort)
}
func test_description_when_libraryNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.libraryNotFound(path)
// When
let got = subject.description
// Then
XCTAssertEqual(got, "The library \(path.pathString) does not exist")
}
func test_type_when_publicHeadersNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.publicHeadersNotFound(path)
// When
let got = subject.type
// Then
XCTAssertEqual(got, .abort)
}
func test_description_when_publicHeadersNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.publicHeadersNotFound(path)
// When
let got = subject.description
// Then
XCTAssertEqual(got, "The public headers directory \(path.pathString) does not exist")
}
func test_type_when_swiftModuleMapNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.swiftModuleMapNotFound(path)
// When
let got = subject.type
// Then
XCTAssertEqual(got, .abort)
}
func test_description_when_swiftModuleMapNotFound() {
// Given
let path = AbsolutePath("/libraries/libTuist.a")
let subject = LibraryNodeLoaderError.swiftModuleMapNotFound(path)
// When
let got = subject.description
// Then
XCTAssertEqual(got, "The Swift modulemap file \(path.pathString) does not exist")
}
}
final class LibraryNodeLoaderTests: TuistUnitTestCase {
var libraryMetadataProvider: MockLibraryMetadataProvider!
var subject: LibraryNodeLoader!
override func setUp() {
libraryMetadataProvider = MockLibraryMetadataProvider()
subject = LibraryNodeLoader(libraryMetadataProvider: libraryMetadataProvider)
super.setUp()
}
override func tearDown() {
super.tearDown()
libraryMetadataProvider = nil
subject = nil
}
func test_load_when_the_path_doesnt_exist() throws {
// Given
let path = try temporaryPath()
let libraryPath = path.appending(component: "libTuist.a")
let publicHeadersPath = path.appending(component: "headers")
try FileHandler.shared.createFolder(publicHeadersPath)
// Then
XCTAssertThrowsSpecific(try subject.load(path: libraryPath,
publicHeaders: publicHeadersPath,
swiftModuleMap: nil), LibraryNodeLoaderError.libraryNotFound(libraryPath))
}
func test_load_when_the_public_headers_directory_doesnt_exist() throws {
// Given
let path = try temporaryPath()
let libraryPath = path.appending(component: "libTuist.a")
let publicHeadersPath = path.appending(component: "headers")
try FileHandler.shared.touch(libraryPath)
// Then
XCTAssertThrowsSpecific(try subject.load(path: libraryPath,
publicHeaders: publicHeadersPath,
swiftModuleMap: nil), LibraryNodeLoaderError.publicHeadersNotFound(publicHeadersPath))
}
func test_load_when_the_swift_modulemap_doesnt_exist() throws {
// Given
let path = try temporaryPath()
let libraryPath = path.appending(component: "libTuist.a")
let publicHeadersPath = path.appending(component: "headers")
let swiftModulemapPath = path.appending(component: "tuist.modulemap")
try FileHandler.shared.createFolder(publicHeadersPath)
try FileHandler.shared.touch(libraryPath)
// Then
XCTAssertThrowsSpecific(try subject.load(path: libraryPath,
publicHeaders: publicHeadersPath,
swiftModuleMap: swiftModulemapPath), LibraryNodeLoaderError.swiftModuleMapNotFound(swiftModulemapPath))
}
func test_load_when_all_files_exist() throws {
// Given
let path = try temporaryPath()
let libraryPath = path.appending(component: "libTuist.a")
let publicHeadersPath = path.appending(component: "headers")
let swiftModulemapPath = path.appending(component: "tuist.modulemap")
let architectures: [BinaryArchitecture] = [.armv7, .armv7s]
let linking: BinaryLinking = .dynamic
try FileHandler.shared.createFolder(publicHeadersPath)
try FileHandler.shared.touch(libraryPath)
try FileHandler.shared.touch(swiftModulemapPath)
libraryMetadataProvider.architecturesStub = { path in
XCTAssertEqual(path, libraryPath)
return architectures
}
libraryMetadataProvider.linkingStub = { path in
XCTAssertEqual(path, libraryPath)
return linking
}
// When
let got = try subject.load(path: libraryPath,
publicHeaders: publicHeadersPath,
swiftModuleMap: swiftModulemapPath)
// Then
XCTAssertEqual(got, LibraryNode(path: libraryPath,
publicHeaders: publicHeadersPath,
architectures: architectures,
linking: linking,
swiftModuleMap: swiftModulemapPath))
}
}

View File

@ -0,0 +1,86 @@
import Basic
import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
@testable import TuistSupportTesting
final class XCFrameworkNodeLoaderErrorTests: TuistUnitTestCase {
func test_type_when_xcframeworkNotFound() {
// Given
let subject = XCFrameworkNodeLoaderError.xcframeworkNotFound("/frameworks/tuist.xcframework")
// Then
XCTAssertEqual(subject.type, .abort)
}
func test_description_when_xcframeworkNotFound() {
// Given
let subject = XCFrameworkNodeLoaderError.xcframeworkNotFound("/frameworks/tuist.xcframework")
// Then
XCTAssertEqual(subject.description, "Couldn't find xcframework at /frameworks/tuist.xcframework")
}
}
final class XCFrameworkNodeLoaderTests: TuistUnitTestCase {
var xcframeworkMetadataProvider: MockXCFrameworkMetadataProvider!
var subject: XCFrameworkNodeLoader!
override func setUp() {
xcframeworkMetadataProvider = MockXCFrameworkMetadataProvider()
subject = XCFrameworkNodeLoader(xcframeworkMetadataProvider: xcframeworkMetadataProvider)
super.setUp()
}
override func tearDown() {
super.tearDown()
xcframeworkMetadataProvider = nil
subject = nil
}
func test_load_throws_when_the_xcframework_doesnt_exist() throws {
// Given
let path = try temporaryPath()
let xcframeworkPath = path.appending(component: "tuist.xcframework")
// Then
XCTAssertThrowsSpecific(try subject.load(path: xcframeworkPath), XCFrameworkNodeLoaderError.xcframeworkNotFound(xcframeworkPath))
}
func test_load_when_the_xcframework_exists() throws {
// Given
let path = try temporaryPath()
let xcframeworkPath = path.appending(component: "tuist.xcframework")
let binaryPath = path.appending(RelativePath("tuist.xcframework/whatever/tuist"))
let linking: BinaryLinking = .dynamic
let infoPlist = XCFrameworkInfoPlist.test()
try FileHandler.shared.touch(xcframeworkPath)
xcframeworkMetadataProvider.infoPlistStub = { path in
XCTAssertEqual(xcframeworkPath, path)
return infoPlist
}
xcframeworkMetadataProvider.binaryPathStub = { path, libraries in
XCTAssertEqual(xcframeworkPath, path)
XCTAssertEqual(libraries, infoPlist.libraries)
return binaryPath
}
xcframeworkMetadataProvider.linkingStub = { path in
XCTAssertEqual(binaryPath, path)
return linking
}
// When
let got = try subject.load(path: xcframeworkPath)
// Then
XCTAssertEqual(got, XCFrameworkNode(path: xcframeworkPath,
infoPlist: infoPlist,
primaryBinaryPath: binaryPath,
linking: linking,
dependencies: []))
}
}

View File

@ -1,18 +0,0 @@
import Basic
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class XCFrameworkParserTests: TuistUnitTestCase {
func test_parsing() {
let cache = GraphLoaderCache()
let frameworkPath = fixturePath(path: RelativePath("MyFramework.xcframework"))
let xcFramework = try! XCFrameworkParser.parse(path: frameworkPath, cache: cache)
let architectures = xcFramework.libraries.flatMap { $0.architectures }
XCTAssertEqual([.x8664, .arm64], architectures)
XCTAssertEqual(xcFramework.binaryPath, frameworkPath.appending(RelativePath("ios-x86_64-simulator/MyFramework.framework/MyFramework")))
}
}

View File

@ -46,7 +46,7 @@ final class BuildCopierTests: XCTestCase {
XCTAssertEqual(toPath.glob("*").count, BuildCopier.files.count)
XCTAssertFalse(fileManager.fileExists(atPath: toPath.appending(component: "test").pathString))
}
func test_copy_without_templates() throws {
let fromDir = try TemporaryDirectory(removeTreeOnDeinit: true)
let fromPath = fromDir.path

View File

@ -159,7 +159,7 @@ final class InstallerTests: TuistUnitTestCase {
"-Xswiftc", "-emit-module-interface",
"-Xswiftc", "-emit-module-interface-path",
"-Xswiftc", temporaryDirectory.path.appending(RelativePath(".build/release/ProjectDescription.swiftinterface")).pathString)
try FileHandler.shared.createFolder(temporaryDirectory.path.appending(component: Constants.templatesDirectoryName))
try FileHandler.shared.createFolder(temporaryDirectory.path.appending(RelativePath(".build/release")))
@ -205,7 +205,7 @@ final class InstallerTests: TuistUnitTestCase {
"-Xswiftc", "-emit-module-interface",
"-Xswiftc", "-emit-module-interface-path",
"-Xswiftc", temporaryDirectory.path.appending(RelativePath(".build/release/ProjectDescription.swiftinterface")).pathString)
try FileHandler.shared.createFolder(temporaryDirectory.path.appending(component: Constants.templatesDirectoryName))
try FileHandler.shared.createFolder(temporaryDirectory.path.appending(RelativePath(".build/release")))
@ -220,7 +220,7 @@ final class InstallerTests: TuistUnitTestCase {
let tuistVersionPath = installationDirectory.appending(component: Constants.versionFileName)
XCTAssertTrue(FileHandler.shared.exists(tuistVersionPath))
}
func test_install_when_no_bundled_release_and_no_templates() throws {
let version = "3.2.1"
let temporaryPath = try self.temporaryPath()
@ -252,7 +252,7 @@ final class InstallerTests: TuistUnitTestCase {
"-Xswiftc", "-emit-module-interface",
"-Xswiftc", "-emit-module-interface-path",
"-Xswiftc", temporaryDirectory.path.appending(RelativePath(".build/release/ProjectDescription.swiftinterface")).pathString)
try FileHandler.shared.createFolder(temporaryDirectory.path.appending(RelativePath(".build/release")))
try subject.install(version: version, temporaryDirectory: temporaryDirectory)

View File

@ -0,0 +1,51 @@
import Basic
import Foundation
import TuistCore
import TuistCoreTesting
import TuistSupport
import XcodeProj
import XCTest
@testable import TuistGenerator
@testable import TuistSupportTesting
final class WorkspaceGeneratorIntegrationTests: TuistTestCase {
var subject: WorkspaceGenerator!
override func setUp() {
super.setUp()
subject = WorkspaceGenerator(config: .init(projectGenerationContext: .concurrent))
}
override func tearDown() {
subject = nil
super.tearDown()
}
// MARK: - Tests
func test_generate_stressTest() throws {
// Given
let temporaryPath = try self.temporaryPath()
let projects = (0 ..< 20).map {
Project.test(path: temporaryPath.appending(component: "Project\($0)"),
name: "Test",
settings: .default,
targets: [Target.test(name: "Project\($0)_Target")])
}
let graph = Graph.create(projects: projects,
dependencies: projects.flatMap { project in
project.targets.map { target in
(project: project, target: target, dependencies: [])
}
})
let workspace = Workspace.test(path: temporaryPath,
projects: projects.map(\.path))
// When / Then
try (0 ..< 50).forEach { _ in
_ = try subject.generate(workspace: workspace,
path: temporaryPath,
graph: graph)
}
}
}

View File

@ -9,7 +9,7 @@ import XCTest
@testable import TuistGenerator
@testable import TuistSupportTesting
final class XcodeProjWriterTests: TuistUnitTestCase {
final class XcodeProjWriterTests: TuistTestCase {
private var subject: XcodeProjWriter!
override func setUp() {

View File

@ -1,48 +0,0 @@
import Basic
import Foundation
import TuistCore
import TuistSupport
import XCTest
@testable import TuistGenerator
@testable import TuistSupportTesting
final class EmbedScriptGeneratorIntegrationTests: TuistTestCase {
var subject: EmbedScriptGenerator!
override func setUp() {
super.setUp()
subject = EmbedScriptGenerator()
}
override func tearDown() {
super.tearDown()
subject = nil
}
func test_script() throws {
// Given
let carthagePath = try temporaryFixture("Carthage/")
let frameworkPath = FileHandler.shared.glob(carthagePath, glob: "*.framework").first!
let framework = FrameworkNode(path: frameworkPath)
// When
let got = try subject.script(sourceRootPath: carthagePath, frameworkPaths: [framework.path])
// Then
XCTAssertTrue(got.inputPaths.contains(RelativePath("2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap")))
XCTAssertTrue(got.inputPaths.contains(RelativePath("773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap")))
XCTAssertTrue(got.inputPaths.contains(RelativePath("RxBlocking.framework")))
XCTAssertTrue(got.inputPaths.contains(RelativePath("RxBlocking.framework.dSYM")))
XCTAssertTrue(got.outputPaths.contains("${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxBlocking.framework"))
XCTAssertTrue(got.outputPaths.contains("${DWARF_DSYM_FOLDER_PATH}/RxBlocking.framework.dSYM"))
XCTAssertTrue(got.outputPaths.contains("${BUILT_PRODUCTS_DIR}/2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap"))
XCTAssertTrue(got.outputPaths.contains("${BUILT_PRODUCTS_DIR}/773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap"))
XCTAssertTrue(got.script.contains("install_framework \"RxBlocking.framework\""))
XCTAssertTrue(got.script.contains("install_dsym \"RxBlocking.framework.dSYM\""))
XCTAssertTrue(got.script.contains("install_bcsymbolmap \"2510FE01-4D40-3956-BB71-857D3B2D9E73.bcsymbolmap\""))
XCTAssertTrue(got.script.contains("install_bcsymbolmap \"773847A9-0D05-35AF-9865-94A9A670080B.bcsymbolmap\""))
}
}

View File

@ -37,7 +37,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_generateEmbedPhase() throws {
// Given
var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.absolute(AbsolutePath("/test.framework")))
dependencies.append(GraphDependencyReference.testFramework())
dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework"))
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")
@ -92,7 +92,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_generateEmbedPhase_setupEmbedFrameworksBuildPhase_whenXCFrameworkIsPresent() throws {
// Given
var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.absolute(AbsolutePath("/Frameworks/Test.xcframework")))
dependencies.append(GraphDependencyReference.testXCFramework(path: "/Frameworks/Test.xcframework"))
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")
let sourceRootPath = AbsolutePath("/")
@ -122,9 +122,9 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_setupFrameworkSearchPath() throws {
let dependencies = [
GraphDependencyReference.absolute(AbsolutePath("/Dependencies/A.framework")),
GraphDependencyReference.absolute(AbsolutePath("/Dependencies/B.framework")),
GraphDependencyReference.absolute(AbsolutePath("/Dependencies/C/C.framework")),
GraphDependencyReference.testFramework(path: "/Dependencies/A.framework"),
GraphDependencyReference.testFramework(path: "/Dependencies/B.framework"),
GraphDependencyReference.testFramework(path: "/Dependencies/C/C.framework"),
]
let sourceRootPath = AbsolutePath("/")
@ -312,7 +312,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_generateLinkingPhase() throws {
var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.absolute(AbsolutePath("/test.framework")))
dependencies.append(GraphDependencyReference.testFramework(path: "/test.framework"))
dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework"))
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")
@ -340,7 +340,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_generateLinkingPhase_throws_whenFileReferenceIsMissing() throws {
var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.absolute(AbsolutePath("/test.framework")))
dependencies.append(GraphDependencyReference.testFramework(path: "/test.framework"))
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")
let fileElements = ProjectFileElements()
@ -371,8 +371,8 @@ final class LinkGeneratorErrorTests: XCTestCase {
func test_generateLinkingPhase_sdkNodes() throws {
// Given
let dependencies: [GraphDependencyReference] = [
.sdk("/Strong/Foo.framework", .required),
.sdk("/Weak/Bar.framework", .optional),
.sdk(path: "/Strong/Foo.framework", status: .required),
.sdk(path: "/Weak/Bar.framework", status: .optional),
]
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")

View File

@ -406,7 +406,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase {
xcodeprojPath: project.path.appending(component: "\(project.fileName).xcodeproj"),
sourceRootPath: sourceRootPath)
var dependencies: Set<GraphDependencyReference> = Set()
let precompiledNode = GraphDependencyReference.absolute(project.path.appending(component: "waka.framework"))
let precompiledNode = GraphDependencyReference.testFramework(path: project.path.appending(component: "waka.framework"))
dependencies.insert(precompiledNode)
try subject.generate(dependencyReferences: dependencies,
@ -610,7 +610,7 @@ final class ProjectFileElementsTests: TuistUnitTestCase {
let sdk = try SDKNode(name: "ARKit.framework",
platform: .iOS,
status: .required)
let sdkDependency = GraphDependencyReference.sdk(sdk.path, sdk.status)
let sdkDependency = GraphDependencyReference.sdk(path: sdk.path, status: sdk.status)
// When
try subject.generate(dependencyReferences: [sdkDependency],

View File

@ -13,7 +13,7 @@ final class WorkspaceGeneratorTests: TuistUnitTestCase {
override func setUp() {
super.setUp()
subject = WorkspaceGenerator()
subject = WorkspaceGenerator(config: .init(projectGenerationContext: .serial))
}
override func tearDown() {

View File

@ -32,8 +32,8 @@ final class GraphLinterTests: TuistUnitTestCase {
try FileHandler.shared.createFolder(frameworkAPath)
let frameworkA = FrameworkNode(path: frameworkAPath)
let frameworkB = FrameworkNode(path: frameworkBPath)
let frameworkA = FrameworkNode.test(path: frameworkAPath)
let frameworkB = FrameworkNode.test(path: frameworkBPath)
cache.add(precompiledNode: frameworkA)
cache.add(precompiledNode: frameworkB)
@ -130,8 +130,8 @@ final class GraphLinterTests: TuistUnitTestCase {
try FileHandler.shared.createFolder(frameworkAPath)
let frameworkA = FrameworkNode(path: frameworkAPath)
let frameworkB = FrameworkNode(path: frameworkBPath)
let frameworkA = FrameworkNode.test(path: frameworkAPath)
let frameworkB = FrameworkNode.test(path: frameworkBPath)
cache.add(precompiledNode: frameworkA)
cache.add(precompiledNode: frameworkB)

View File

@ -45,7 +45,7 @@ class StaticProductsGraphLinterTests: XCTestCase {
let framework = Target.test(name: "Framework", product: .framework)
let project = Project.test(targets: [app, framework])
let libraryNode = LibraryNode(path: "/path/to/library", publicHeaders: "/path/to/library/include")
let libraryNode = LibraryNode.test(path: "/path/to/library", publicHeaders: "/path/to/library/include")
let frameworkNode = TargetNode(project: project, target: framework, dependencies: [libraryNode])
let appNode = TargetNode(project: project, target: app, dependencies: [libraryNode, frameworkNode])

View File

@ -0,0 +1,50 @@
import Basic
import Foundation
import TuistCore
import TuistSupport
import XCTest
@testable import TuistGenerator
@testable import TuistSupportTesting
final class EmbedScriptGeneratorTests: TuistUnitTestCase {
var subject: EmbedScriptGenerator!
override func setUp() {
super.setUp()
subject = EmbedScriptGenerator()
}
override func tearDown() {
super.tearDown()
subject = nil
}
func test_script() throws {
// Given
let path = AbsolutePath("/frameworks/tuist.framework")
let dsymPath = AbsolutePath("/frameworks/tuist.dSYM")
let bcsymbolPath = AbsolutePath("/frameworks/tuist.bcsymbolmap")
let framework = GraphDependencyReference.testFramework(path: path,
binaryPath: path.appending(component: "tuist"),
dsymPath: dsymPath,
bcsymbolmapPaths: [bcsymbolPath])
// When
let got = try subject.script(sourceRootPath: framework.path!.parentDirectory, frameworkReferences: [framework])
// Then
XCTAssertEqual(got.inputPaths, [
RelativePath(path.basename),
RelativePath(dsymPath.basename),
RelativePath(bcsymbolPath.basename),
])
XCTAssertEqual(got.outputPaths, [
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/\(path.basename)",
"${DWARF_DSYM_FOLDER_PATH}/tuist.dSYM", "${BUILT_PRODUCTS_DIR}/\(bcsymbolPath.basename)",
])
XCTAssertTrue(got.script.contains("install_framework \"\(path.basename)\""))
XCTAssertTrue(got.script.contains("install_dsym \"\(dsymPath.basename)\""))
XCTAssertTrue(got.script.contains("install_bcsymbolmap \"\(bcsymbolPath.basename)\""))
}
}

View File

@ -1,16 +1,17 @@
import Basic
import Foundation
import TuistCore
@testable import TuistGenerator
@testable import TuistSupportTesting
final class MockEmbedScriptGenerator: EmbedScriptGenerating {
var scriptArgs: [(AbsolutePath, [AbsolutePath])] = []
var scriptArgs: [(AbsolutePath, [GraphDependencyReference])] = []
var scriptStub: Result<EmbedScript, Error>?
func script(sourceRootPath: AbsolutePath,
frameworkPaths: [AbsolutePath]) throws -> EmbedScript {
scriptArgs.append((sourceRootPath, frameworkPaths))
frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript {
scriptArgs.append((sourceRootPath, frameworkReferences))
if let scriptStub = scriptStub {
switch scriptStub {
case let .failure(error): throw error

View File

@ -1,5 +1,6 @@
import Basic
import TuistCore
import TuistCoreTesting
import TuistSupport
import XcodeProj
import XCTest
@ -8,7 +9,7 @@ import XCTest
@testable import TuistSupport
@testable import TuistSupportTesting
final class StableXcodeProjIntegrationTests: TuistUnitTestCase {
final class StableXcodeProjIntegrationTests: TuistTestCase {
override func setUp() {
super.setUp()
@ -59,7 +60,14 @@ final class StableXcodeProjIntegrationTests: TuistUnitTestCase {
let subject = DescriptorGenerator()
let writer = XcodeProjWriter()
let linter = GraphLinter()
let graphLoader = GraphLoader(modelLoader: try createModelLoader())
let frameworkNodeLoader = MockFrameworkNodeLoader()
let libraryNodeLoader = MockLibraryNodeLoader()
let xcframeworkNodeLoader = MockXCFrameworkNodeLoader()
let graphLoader = GraphLoader(modelLoader: try createModelLoader(),
frameworkNodeLoader: frameworkNodeLoader,
xcframeworkNodeLoader: xcframeworkNodeLoader,
libraryNodeLoader: libraryNodeLoader)
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)
try linter.lint(graph: graph).printAndThrowIfNeeded()

View File

@ -1,14 +1,14 @@
import Basic
import Foundation
import SPMUtility
import TuistSupport
import TuistScaffold
import TuistSupport
import XCTest
@testable import TuistKit
@testable import TuistSupportTesting
@testable import TuistScaffoldTesting
@testable import TuistLoaderTesting
@testable import TuistScaffoldTesting
@testable import TuistSupportTesting
final class InitCommandTests: TuistUnitTestCase {
var subject: InitCommand!

View File

@ -0,0 +1,72 @@
import Foundation
import XCTest
@testable import TuistSupport
final class ArrayExecutionContextTests: XCTestCase {
func test_concurrentMap_success() {
// Given
let numbers = Array(0 ... 1000)
let transform: (Int) -> String = { "Number \($0)" }
// When
let results = numbers.map(context: .concurrent, transform)
// Then
XCTAssertEqual(results, numbers.map(transform))
}
func test_concurrentMap_errors() throws {
// Given
let numbers = Array(0 ... 1000)
let transform: (Int) throws -> String = {
guard $0 % 100 == 0 else {
throw TestError.someError
}
return "Number \($0)"
}
// When / Then
XCTAssertThrowsSpecific(try numbers.map(context: .concurrent, transform), TestError.someError)
}
func test_concurrentForEach_success() {
// Given
let numbers = Array(0 ... 1000)
var performedNumbers = Set<Int>()
let queue = DispatchQueue(label: "TestQueue")
let perform: (Int) -> Void = { number in
queue.async {
performedNumbers.insert(number)
}
}
// When
numbers.forEach(context: .concurrent, perform)
// Then
let resuls = queue.sync {
performedNumbers
}
XCTAssertEqual(resuls, Set(numbers))
}
func test_concurrentForEach_error() {
// Given
let numbers = Array(0 ... 1000)
let perform: (Int) throws -> Void = {
guard $0 % 100 == 0 else {
throw TestError.someError
}
}
// When / Then
XCTAssertThrowsSpecific(try numbers.forEach(context: .concurrent, perform), TestError.someError)
}
// MARK: - Helpers
private enum TestError: Error {
case someError
}
}

View File

@ -7,6 +7,7 @@ module.exports = {
title: title,
description: `Tuist is a tool that helps developers manage large Xcode projects by leveraging project generation. Moreover, it provides some tools to automate most common tasks, allowing developers to focus on building apps.`,
siteUrl: siteUrl,
discourseUrl: "https://community.tuist.io",
githubUrl: 'https://github.com/tuist',
releasesUrl: 'https://github.com/tuist/tuist/releases',
documentationUrl: 'https://docs.tuist.io/',
@ -26,6 +27,12 @@ module.exports = {
`gatsby-plugin-theme-ui`,
`gatsby-transformer-yaml`,
`gatsby-plugin-react-helmet`,
{
resolve: `gatsby-plugin-google-analytics`,
options: {
trackingId: "UA-125584790-1",
},
},
{
resolve: `gatsby-source-filesystem`,
name: 'data',
@ -167,6 +174,7 @@ module.exports = {
remarkPlugins: [remarkSlug],
gatsbyRemarkPlugins: [
`gatsby-remark-smartypants`,
`gatsby-remark-copy-linked-files`,
{
resolve: `gatsby-remark-images`,
options: {

View File

@ -0,0 +1,98 @@
/** @jsx jsx */
import { jsx, Styled, useThemeUI, useColorMode } from 'theme-ui'
import { useResponsiveValue } from '@theme-ui/match-media'
import { Graphviz } from 'graphviz-react';
const useTextColor = () => {
const { theme } = useThemeUI()
const [colorMode, _] = useColorMode()
if (colorMode === 'light' || colorMode === 'default') {
return theme.colors.text
} else {
return theme.colors.modes[colorMode].text
}
}
const useBackgroundColor = () => {
const { theme } = useThemeUI()
const [colorMode, _] = useColorMode()
if (colorMode === 'light' || colorMode === 'default') {
return theme.colors.background
} else {
return theme.colors.modes[colorMode].background
}
}
const CrossPlatform = () => {
const textColor = useTextColor()
const backgroundColor = useBackgroundColor()
const width = useResponsiveValue(["100%", "600"])
return <Graphviz options={{ width: width }} dot={`digraph {
graph [bgcolor="${backgroundColor}"];
µSearchiOS [color="${textColor}", fontcolor="${textColor}"];
µSearchmacOS [color="${textColor}", fontcolor="${textColor}"];
µSearchwatchOS [color="${textColor}", fontcolor="${textColor}"];
µSearch [color="${textColor}", fontcolor="${textColor}"];
µSearchiOS -> µSearch [color="${textColor}"];
µSearchmacOS -> µSearch [color="${textColor}"];
µSearchwatchOS -> µSearch [color="${textColor}"];
}`} />
}
const MicroFeature = () => {
const textColor = useTextColor()
const backgroundColor = useBackgroundColor()
const width = useResponsiveValue(["100%", "300"])
return <div sx={{ py: 2 }}>
<Graphviz options={{ width: width }} dot={`digraph {
graph [bgcolor="${backgroundColor}"];
Example [color="${textColor}", fontcolor="${textColor}"];
Source [color="${textColor}", fontcolor="${textColor}"];
Testing [color="${textColor}", fontcolor="${textColor}"];
Tests [color="${textColor}", fontcolor="${textColor}"];
Example -> Source [color="${textColor}"];
Example -> Testing [color="${textColor}"];
Tests -> Source [color="${textColor}"];
Tests -> Testing [color="${textColor}"];
Testing -> Source [color="${textColor}"];
}`} />
</div>
}
const Layers = () => {
const textColor = useTextColor()
const backgroundColor = useBackgroundColor()
const width = useResponsiveValue(["100%", "400"])
return <div sx={{ py: 3 }}>
<Graphviz options={{ width: width }} dot={`digraph {
graph [bgcolor="${backgroundColor}"];
Application [color="${textColor}", fontcolor="${textColor}"];
µSearch [color="${textColor}", fontcolor="${textColor}"];
µHome [color="${textColor}", fontcolor="${textColor}"];
µProfile [color="${textColor}", fontcolor="${textColor}"];
µDependencies [color="${textColor}", fontcolor="${textColor}"];
µCore [color="${textColor}", fontcolor="${textColor}"];
µUI [color="${textColor}", fontcolor="${textColor}"];
µTesting [color="${textColor}", fontcolor="${textColor}"];
Application -> µSearch [color="${textColor}"];
Application -> µHome [color="${textColor}"];
Application -> µProfile [color="${textColor}"];
µSearch -> µDependencies [color="${textColor}"];
µHome -> µDependencies [color="${textColor}"];
µProfile -> µDependencies [color="${textColor}"];
µDependencies -> µCore [color="${textColor}"];
µDependencies -> µUI [color="${textColor}"];
µDependencies -> µTesting [color="${textColor}"];
}`} />
</div>
}
export { CrossPlatform, MicroFeature, Layers }

View File

@ -0,0 +1,283 @@
---
name: µFeatures Architecture
order: 1
excerpt: 'This document describes an approach for architecting a modular Apple OS application to enable scalability, optimize build and test cycles, and ensure good practices.'
---
import { CrossPlatform, MicroFeature, Layers } from "./components/microfeatures"
# µFeatures Architecture
## What
uFeatures is an architectural approach to structure Apple OS applications to enable scalability, optimize build and test cycles, and ensure good practices in your team. Its core idea is to build your apps by building independent features that are interconnected using clear and concise APIs.
These guidelines introduce the principles of the architecture, helping you identify and organize your application features in different layers. It also introduces tips, tools and advice if you decide to use this architecture.
<Message
info
title="Name"
description={`The name uFeatures *(microfeatures)* comes from the [microservices architecture](https://martinfowler.com/articles/microservices.html), where different "backend features" run as different services with defined APIs to enable communication between them.`}
/>
## Context
Apps are made of features. Typically these features are part of the same module, or target where the whole application is defined. The natural inclination in the team is to continue building features and its tests in the same targets. As a result, the application and its tests target grows in complexity which manifests in bugs, bad compilation times, and team performance. What seemed to be a good architecture, doesn't work out that well in large codebases or teams.
This is frequently a big source of frustration when it comes to work on those projects. The time we spend goes into compiling rather than building and experimenting with the platform.
## Motivation
The µFeatures's main motivation is to support the scalability of large iOS codebases leveraging platform features and tools. There are other solutions out there that could be also be considered to overcome those issues. A very popular one nowadays is [React Native](https://facebook.github.io/react-native/) that leverages the Javascript dynamism to offer developers a pleasant experience working in the code base, but at the same time a native experience from the user point of view.
**We believe that the usage of native tools and technologies can be optimized to overcome scalability challenges that sooner or later show up in our projects**
## Before reading
- Don't expect this to be a silver-bullet solution to your problems. You should take the core ideas, process them, and apply the principles to your projects.
- Each project is different, and so are the needs. With the ideas in the guidelines, and your needs, you should figure out what might work out for you.
- Since everything this architecture depends on is evolving *(tools, languages, concepts)*, the guidelines might get outdated very quickly. If that happens, don't hesitate to open a PR and contribute with keeping this guidelines up to date.
- It can very tempting to scale your app architecture before it actually needs it. If your app needs it, you'll notice it, and only at that point, you should consider start tackling the issue.
## Core principle
Developers should be able to **build, test and try** their features fast, with independence of the main app.
## What is a µFeature
A µFeature represents an application feature and is a combination of the following 4 targets *(referring with target to a Xcode target)*:
- **Source:** Contains the feature source code *(Swift, Objective-C, C++, React Native...)* and its resources *(images, fonts, storyboards, xibs)*.
- **Tests:** Contains the feature unit and integration tests.
- **Testing:** Provides testing data that can be used for the tests and from the example app. It also provide mocks for uFeature classes and protocols that can be used by other features as we'll see later.
- **Example:** Contains an example app that developers can use to try out the feature under certain conditions *(different languages, screen sizes, settings)*.
The diagram below shows the 4 targets and the dependencies between them:
<MicroFeature/>
## Why a µFeature
### Clear and concise APIs
When all the app source code lives in the same target is very easy to build implicit dependencies in code, and end up with the so well-known spaghetti code. Everything is strongly coupled, the state is sometimes unpredictable, and introducing new changes become a nightmare. When we define features in independent targets we need to design public APIs as part of our feature implementation. We need to decide what should be public, how our feature should be consumed, what should remain private. We have more control over how we want our feature *"clients"* to use the feature and we can enforce good practises by designing safe APIs.
### Small modules
[Divide and conquer](https://en.wikipedia.org/wiki/Divide_and_conquer). Working in small modules allows you to have more focus and test and try the feature in isolation. Moreover, development cycles are much faster since we have a more selective compilation, compiling only the components that are necessary to get our feature working. The compilation of the whole app is only necessary at the very end of our work, when we need to integrate the feature into the app.
### Reusability
Reusing code across apps and other products like extensions is encouraged using frameworks or libraries. By building µFeatures reusing them is pretty straightforward. We can build an iMessage extension, a Today Extension, or a watchOS application by just combining existing µFeatures and adding *(when necessary)* platform-specific UI layers.
## Types of µFeatures
### Foundation
Foundation µFeatures contain foundational tools *(wrappers, extensions, ...)* that are combined to build other µFeatures. Thus other µFeatures have access to the foundation ones. Some examples of foundations µFeatures are:
- **µUI:** Provides custom views, UIKit extensions, fonts, and colors that are used to build user-facing layouts.
- **µTesting:** Facilitates testing by providing XCTest extensions as well as custom assertions.
- **µCore:** It can be seen as the `Foundation` of your app, providing tools such as analytics reporter, logger, API client or a storage class.
In practice, foundation µFeatures expose interfaces (Structs, Classes, Enums) and extensions of platform frameworks such as `XCTest`, `Foundation` or `UIKit`.
<Message
warning
title="Static instances"
description="Foundation µFeatures shouldn't expose static instances that are globally accessed. As we'll see later, it's up to the app to control the lifecycle of those foundation dependencies, and pass them to other µFeatures using dependency injection."
/>
### Product
Product µFeatures contain features that the user can feel and interact with. They are built by combining foundation µFeatures. Some examples of product µFeatures are:
- **µSearch:** Contains your product search feature that allows users searching content on the platform.
- **µPayments:** Contains the business logic to handle payment flows and upsell screens to upgrade users to premium plans.
- **µHome:** Contains the product home screen with the most recent platform content.
<Message
warning
title="Product domain"
description="Product µFeatures usually represent your product's features."
/>
In practice, product µFeatures expose **views** and **services**. In the following sections we'll see how the app target uses those views and services to build up the app.
## Hooking µFeatures
As we mentioned earlier, µFeatures **don't expose instances** and it's the app responsibility to create instances and use them. How we instantiate and hook µFeatures depends on the type of µFeature.
### Services
Apps usually have services or utils whose state is tied to the application lifecycle. Those instances are global and the majority of the features will need to access them.
```swift
// Services.swift in the main application
import uCore
import uPlayback
class Services {
static let playback = PlaybackService() // From uPlayback
static let client = Client(baseUrl: "https://api.shakira.io") // From uCore
static let analytics = Analytics(firebaseKey: "xxx") // From uCore
}
```
In the example above, `Services.swift` works as a static container, initializing all the services and tools with their initial state. Some of these services might need to know about the application lifecycle. We could subscribe to those notifications internally, but then we'd be coupling the service to the `NotificationCenter` and the platform-specific lifecycle notifications. What we could do instead is explicitly notifying them about the lifecycle events from the app delegate.
```swift
// AppDelegate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidBecomeActive(_ application: UIApplication) {
Services.playback.restoreState()
}
}
```
### Views/ViewControllers
On the other side, product µFeatures can also expose views and view controllers. These views usually encapsulate the logic to update themselves according to internal state changes, and react to user actions, turning those actions into state updates *(e.g. synchronizing data with the API)*.
```swift
// Home.swift in uHome
import UIKit
import uCore
public class Home {
let client: Client
public init(client: Client) {
self.client = client
}
public func makeViewController(delegate: HomeDelegate) -> UIViewController {
return HomeViewController(client: client, delegate: delegate)
}
}
```
The example above shows how the home µFeature looks like. It gets initialized with its dependencies and expose a method that instantiates and returns a view controller to be used from the app. Notice that the method returns a `UIViewController` instead of a `HomeViewController`. By doing that we abstract the app from any implementation detail.
#### Delegating navigation
You might have noticed that we pass a delegate when we instantiate the view controller. The delegate responds to actions that trigger a navigation to a different µFeature. It's up to the app to define the navigation between different µFeatures. A pattern that works very well here is the [Coordinator Pattern](https://vimeo.com/144116310) that allows you represent your navigation as a tree of coordinators. These coordinators would be in the app, responding to µFeatures actions, and triggering the navigation to other coordinators.
Delegating the navigation to the app gives us the flexibility to change the navigation based on the product where we are consuming the µFeature from. Let's take an hypothetical search µFeature that exposes a search view controller. When we use that view controller from the app, we want to navigate to another µFeature when the user taps in one of the search results. However, if we use that view controller from an iMessage extension, we want the action to be different, and instead, share the search result with one of your contacts.
## Layers
We talked about µFeatures, types, and how to use them from the app, but we missed out a very important point, how to organize them to prevent ending up with a messy dependency graph or eventually with circular dependencies impossible to resolve for the compiler.
It's recommended to organize the µFeatures in three layers below the product layer as shown in the image below.
<Layers/>
- **Product µFeatures:** Contains all the product µFeatures.
- **Dependency inversion:** Contains the product µFeatures public interfaces.
- **Foundation µFeatures:** Contains all the foundation µFeatures.
Product µFeatures don't depend on each other, instead we use the **dependency inversion principle** to expose their interfaces in a layer underneath them. There are a couple of advantages that support it:
- It delegates to the app the decission about how different features are connected. We could decide in a product-basis.
- It decouples the targets in the same layer, removing the need to compile all the dependencie when we try to compile a single product µFeature *(faster compilation)*.
## Dependencies
As soon as you start building µFeatures you'll realize that most of the features need dependencies that are injected from the app. We could inject those dependencies in the constructor but we'd end up with constructors with a long list of parameters being passed. Instead, we could leverage protocols to represent the µFeatures dependencies and pass them easily *(credits to [@andreacipriani](https://github.com/andreacipriani) for coming up with this approach)*:
```swift
public protocol BaseDependencies {
func makeClient() -> Client
func makeLogger() -> Logger
}
```
A protocol defines the base dependencies that are the most common dependencies across all the features. Dependencies are exposed through methods that return the dependency as a return parameter of those methods.
```swift
class AppDependencies: BaseDependencies {
func makeClient() -> Client {
return Services.client
}
func makeLogger() -> Logger {
return Services.logger
}
}
```
From the app we conform the `BaseDependencies` protocol, defining a class, `AppDependencies` that represents our application base dependencies.
```swift
public protocol SearchDependencies: BaseDependencies {
func makeAnalytics() -> Analytics
}
```
For some particular µFeatures, we might need some extra dependencies. We can define those in a new protocol that conforms the `BaseDependencies` protocol, adding the extra dependencies. In the example below `SearchDependencies` exposes also an `Analytics` dependency.
```swift
public final class SearchBuilder {
private let dependenciesSolver: SearchDependencies
public init(dependenciesSolver: SearchDependencies) {
self.dependenciesSolver = dependenciesSolver
}
public func makeViewController() -> UIViewController {
let client = dependenciesSolver.makeClient()
let logger = dependenciesSolver.makeLogger()
let analytics = dependenciesSolver.makeAnalytics()
return SearchViewController(client: client, logger: logger, analytics: analytics)
}
}
// From the app
let searchBuilder = SearchBuilder(dependenciesSolver: AppDependencies())
```
The example above shows how we can inject dependencies in a builder that builds the µFeature instance, in this case a `UIViewController`.
<Message
info
title = "Alternatives"
description = "This is just an example of how we can simplify dependency injection. There are other alternatives out there. It's up to you to pick up the one that works best for your project and setup."/>
## Cross-platform µFeatures
Your product might be available in different platforms or from different products. In that case it's very important that we reuse as much code as possible. `Foundation` APIs are very similar across plaftorms, with just some subtle differences, but UI frameworks like `UIKit` or `AppKit` are not. We could have multiplatform µFeatures by using [Swift macros](http://ericasadun.com/2014/06/06/swift-cross-platform-code/) that conditionally compile UI for each of the platforms. However, Xcode is very bad dealing with those macros and the syntax highlghting and the autocompletion don't work very well.
There is something we can do though to enable cross-platform µFeatures. Splitting business logic and UI in two different layers, one that contains the business layer and is cross-platform, and another one that contains the UI and that is platform/product specific.
<CrossPlatform/>
The image above shows how the Search µFeature is split into the business logic target, `µSearch` and the platform specific ones `µSearchiOS` and `µSearchmacOS`. Both UI frameworks depend on `µSearch`. Each of those targets would have their corresponding tests target.
<Message
info
title = "Building cross-platform frameworks"
description = "You can read more about how to setup frameworks to be cross-platform no the [following link](http://ilya.puchka.me/xcode-cross-platform-frameworks/)."/>
## Example app
We are working on defining an example app with Tuist. Stay tuned!
## Frequently asked questions
#### One or multiple Git repositories?
If you are working with git branches, we recommend you to keep everything in the same repository for convenience reasons. Facebook is a good example of a huge company keeping all the project in a single repositories and Uber [wrote about it](https://eng.uber.com/ios-monorepo/) a year ago.
#### How do you version µFeatures?
If µFeatures are part of the same repository, they are versioned with the app. If you have them in different repositories you can use Git Submodules, Carthage, or your own dependency resolver to fetch specific versions of your µFeatures to link from the app.
#### External dependencies?
This architecture doesn't limit you from using external dependencies. There's one thing to keep in mind though: you won't be able to use [CocoaPods](https://cocoapods.org) dependencies with your µFeatures. CocoaPods is not able to analyze your dependencies tree, and setup all the targets in the stack accordingly. If you want to use an external dependency from a µFeature framework, we recommend you to use [Carthage](https://github.com/carthage).
#### Can I include resources?
You can, but remember that you can only do it if your µFeature is a framework and not a library.
## Resources
- [Building µFeatures](https://speakerdeck.com/pepibumur/building-ufeatures)
- [Framework Oriented Programming](https://speakerdeck.com/pepibumur/framework-oriented-programming-mobilization-dot-pl)
- [A Journey into frameworks and Swift](https://speakerdeck.com/pepibumur/a-journey-into-frameworks-and-swift)
- [Leveraging frameworks to speed up our development on iOS - Part 1](https://developers.soundcloud.com/blog/leveraging-frameworks-to-speed-up-our-development-on-ios-part-1)
- [Library Oriented Programming](https://academy.realm.io/posts/justin-spahr-summers-library-oriented-programming/)
- [Building Modern Frameworks](https://developer.apple.com/videos/play/wwdc2014/416/)
- [The Unofficial Guide to xcconfig files](https://pewpewthespells.com/blog/xcconfig_guide.html)
- [Static and Dynamic Libraries](https://pewpewthespells.com/blog/static_and_dynamic_libraries.html)

View File

@ -0,0 +1,40 @@
---
name: Cloud
excerpt: 'Learn more about what cloud features are, why the are important for scaling up projects, and why they need a server-side component that holds state across project generations, builds, and team mebers.'
---
# Cloud
## Context
Easing the maintenance of large and modular Xcode projects is **not the only challenge** teams run into when scaling up projects.
Other challenges that they face are keeping the build and test times slow, both locally and on CI *(i.e. faster feedback)*,
and ensuring that the projects and the derived artifacts *(e.g. app bundles)* meet some quality standards.
For instance,
in most Xcode projects,
when developers open pull/merge requests,
they don't have a way to know if they are negatively impacting the quality of the project:
*build times are under a reasonable value, or
the size of the apps is acceptable.*
Tuist can leverage the generation of Xcode projects to help teams with those challenges.
However,
**it needs a server side component** that holds a state across project generations, builds, and all the team members.
The subset of features that require that server side component are referred to as **cloud**.
<Message
info
title="Work in progress"
description={`We are currently working on the implementation of the first two cloud features: *caching and insights*. If you'd like to stay up to date with the progress that we are making, we suggest following us on [Twitter](https://twitter.com/tuistio).`}/>
## Server-agnostic
Tuist provides the client-side logic for those features,
and defines the contract with the server-side component through an HTTP [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer).
That gives the teams and projects the flexibility to choose the implementation that they'd like to use,
and also decide if they'd like to host it internally.
More information about the contract can be found on [this page](/docs/cloud/contract/).

View File

@ -0,0 +1,68 @@
---
name: Contract
excerpt: ''
---
# Contract
In order for Tuist's cloud features to interact with HTTP servers,
the exposed API needs to conform with the contract described in this page.
<Message
info
title="Base URL"
description={`In all the examples below, we are assuming that the project is pointing to a server with URL https://cloud.tuist.io`}/>
## Authentication
### User authentication
When using Tuist's cloud features from a local environments,
users will need to authenticate with the API.
If the authentication is successful,
a token will be generated and stored safely in the user's environment.
The generated token must be unique to the user and valid across all the projects the user has access to.
The authentication flow:
1. When the user runs `tuist cloud auth`, Tuist sends the user to `https://cloud.tuist.io/auth`.
2. The user authenticates on the server.
3. When the authentication finishes, the server redirects the user to `http://localhost:4455/auth`.
The URL that triggers the call to localhost should include the any of the following attributes:
- **token:** The generated token if the authentication was successful.
- **error:** The error message if the authentication failed.
```bash
# A successful authentication
http://localhost:4455/auth?token=xyz
# An errored authentication
# Notice that the error argument has been escaped.
http://localhost:4455/auth?error=Couldn%27t%20find%20the%20current%20user
```
<Message
info
title="Authenticating users"
description={`How the server decides to authenticate the user is up to the server. However, we recommend authenticating using the version control provider *(e.g. GitHub)*. That'll allow tying the project to a repository and leverage the API to know whether the user has access to the repository and therefore the project.`}/>
### Continuous integration (CI) authentication
On CI,
projects will authenticate using a secret token that is associated to the project.
The variable should be present in the environment with the name `TUIST_CLOUD_TOKEN`.
For security reasons,
we recommend defining that variable as a secret variables.
### Authentication header
Tuist will authenticate HTTP requests by including an `Authorization` header:
```bash
curl -v -H "Authorization: token TOKEN" https://cloud.tuist.io/api/...
```
Depending on the endpoint, the token will represent a project or a user.
Each of the endpoints documented in the following sections will indicate what type of token is expected.

View File

@ -0,0 +1,40 @@
/** @jsx jsx */
import { jsx, MenuButton } from 'theme-ui'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faCloud,
faUsers,
faFileCode,
faLayerGroup,
} from '@fortawesome/free-solid-svg-icons'
const Cloud = () => {
return (
<FontAwesomeIcon sx={{ width: 20, height: 20 }} icon={faCloud} size="sm" />
)
}
const Users = () => {
return (
<FontAwesomeIcon sx={{ width: 20, height: 20 }} icon={faUsers} size="sm" />
)
}
const Contributors = () => {
return (
<FontAwesomeIcon
sx={{ width: 20, height: 20 }}
icon={faFileCode}
size="sm"
/>
)
}
const Architectures = () => {
return (
<FontAwesomeIcon
sx={{ width: 20, height: 20 }}
icon={faLayerGroup}
size="sm"
/>
)
}
export { Cloud, Users, Contributors, Architectures }

View File

@ -11,7 +11,14 @@ const Message = ({ title, description }) => {
<div sx={{ fontWeight: 'heading', fontSize: 2 }}>
<span>{title}</span>
</div>
<ReactMarkdown source={description} />
<ReactMarkdown source={description} sx={{
"a:-webkit-any-link": {
color: "primary",
":hover,:focus,:visited": {
color: "secondary",
},
},
}} />
</ThemeUIMessage>
)
}

View File

@ -87,3 +87,64 @@ And last but not least,
our test cases should contemplate failing scenarios.
Not only they ensure that we are handling errors as we are supposed to,
but prevent future developers from breaking that logic.
## 7. Write code for humans
The code that we write can make a huge difference between a buggy and unmaintainable code base,
and a stable and maintainable one.
Write beautiful and concise code that is **easy to read and understand**.
Leverage the abstractions and primitives provided by the programming language,
Swift,
to create a solid structure made of simple pieces with scoped responsibilities.
Don't add code to Tuist that reads like a long and mysterious bash script.
The programming patterns and paradigms that you might apply when building apps might apply to CLI too.
In fact, a pattern like MVP, is also valid in the context of CLIs with the difference that the view is the CLI output *(chunks of data sent through the standard output and error)*.
Code that reads like a book encourages contributions,
and contributions bring new ideas to the table that
We write code not just to be understood by the computer or other programmers, but to bask in the warm glow of beauty. Aesthetically pleasing code is a value unto itself and should be pursued with vigor. That doesnt mean that beautiful code always trumps other concerns, but it should have a full seat at the table of priorities.
So what is beautiful code? In Ruby, its often somewhere at the intersection between native Ruby idioms and the power of a custom domain-specific language. Its a fuzzy line, but one well worth trying to dance.
Heres a simple example from Active Record:
class Project < ApplicationRecord
belongs_to :account
has_many :participants, class_name: 'Person'
validates_presence_of :name
end
This looks like DSL, but its really just a class definition with three class-method calls that take symbols and options. Theres nothing fancy here. But it sure is pretty. It sure is simple. It gives an immense amount of power and flexibility from those few declarations.
Part of the beauty comes from these calls honoring the previous principles, like Convention over Configuration. When we call belongs_to :account, were assuming that the foreign key is called account_id and that it lives in the projects table. When we have to designate the class_name of Person to the role of the participants association, we require just that class name definition. From it well derive, again, the foreign keys and other configuration points.
Heres another example from the database migrations system:
class CreateAccounts < ActiveRecord::Migration
def change
create_table :accounts do |t|
t.integer :queenbee_id
t.timestamps
end
end
end
This is the essence of framework power. The programmer declares a class according to certain convention, like a ActiveRecord::Migration subclass that implements #change, and the framework can do all the plumbing that goes around that, and know this is the method to call.
This leaves the programmer with very little code to write. In the case of migrations, not only will this allow a call to rails db:migrate to upgrade the database to add this new table, itll also allow it to go the other way of dropping this table with another call. This is very different from a programmer making all this happen and stitching the workflow together from libraries they call themselves.
Sometimes beautiful code is more subtle, though. Its less about making something as short or powerful as possible, but more about making the rhythm of the declaration flow.
These two statements do the same:
if people.include? person
if person.in? people
But the flow and focus is subtlety different. In the first statement, the focus is on the collection. Thats our subject. In the second statement, the subject is clearly the person. Theres not much between the two statements in length, but Ill contend that the second is far more beautiful and likely to make me smile when used in a spot where the condition is about the person.

View File

@ -1,3 +1,7 @@
import { Cloud, Users, Contributors, Architectures } from "./components/icons"
<Users/>
- [Users](/docs/usage/getting-started/)
- [Getting started](/docs/usage/getting-started/)
- [Project & workspace](/docs/usage/projectswift/)
@ -11,6 +15,22 @@
<br></br>
<Cloud/>
- [Cloud](/docs/cloud/cloud/)
- [Contract](/docs/cloud/contract/)
<br></br>
<Architectures/>
- [Architectures](/docs/architectures/microfeatures/)
- [µFeatures](/docs/architectures/microfeatures/)
<br></br>
<Contributors/>
- [Contributors](/docs/contribution/getting-started/)
- [Getting started](/docs/contribution/getting-started/)
- [Code reviews](/docs/contribution/code-reviews/)

View File

@ -15,6 +15,7 @@
"@mdx-js/react": "^1.5.7",
"@mdx-js/tag": "^0.20.3",
"@theme-ui/color": "^0.3.1",
"@theme-ui/match-media": "^0.3.1",
"@theme-ui/presets": "^0.3.0",
"@theme-ui/prism": "^0.3.0",
"@theme-ui/sidenav": "^0.3.1",
@ -25,6 +26,7 @@
"gatsby-image": "^2.2.44",
"gatsby-plugin-favicon": "3.1.6",
"gatsby-plugin-feed": "^2.3.28",
"gatsby-plugin-google-analytics": "^2.1.38",
"gatsby-plugin-manifest": "2.2.48",
"gatsby-plugin-mdx": "^1.0.83",
"gatsby-plugin-meta-redirect": "^1.1.1",
@ -39,11 +41,13 @@
"gatsby-plugin-theme-ui": "^0.3.0",
"gatsby-redirect-from": "^0.2.1",
"gatsby-remark-check-links": "^2.1.0",
"gatsby-remark-copy-linked-files": "^2.1.40",
"gatsby-remark-images": "^3.1.50",
"gatsby-remark-smartypants": "^2.1.23",
"gatsby-remark-social-cards": "https://github.com/pepibumur/gatsby-remark-social-cards.git#0.5.2",
"gatsby-source-filesystem": "^2.1.55",
"gatsby-transformer-yaml": "^2.2.27",
"graphviz-react": "^1.0.4",
"is-absolute-url": "^3.0.3",
"moment": "^2.24.0",
"polished": "^3.4.4",

View File

@ -6,6 +6,7 @@ import { Location } from '@reach/router'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faGithub } from '@fortawesome/free-brands-svg-icons'
import { faSlack } from '@fortawesome/free-brands-svg-icons'
import { faDiscourse } from '@fortawesome/free-brands-svg-icons'
import logo from '../../static/logo.svg'
const ColorButton = ({ mode, ...props }) => (
@ -74,12 +75,13 @@ export default ({ menuOpen, setMenuOpen, menuRef }) => {
}
const {
site: {
siteMetadata: { githubUrl, slackUrl, firstDocumentationPagePath },
siteMetadata: { githubUrl, discourseUrl, slackUrl, firstDocumentationPagePath },
},
} = useStaticQuery(graphql`
query {
site {
siteMetadata {
discourseUrl
githubUrl
slackUrl
firstDocumentationPagePath
@ -187,7 +189,7 @@ export default ({ menuOpen, setMenuOpen, menuRef }) => {
</Link>
</div>
<div sx={{ flexDirection: 'row', display: 'flex' }}>
<div sx={{ flexDirection: 'row', display: 'flex', mt: [3, 0] }}>
<a
sx={{
...linkStyle,
@ -206,6 +208,24 @@ export default ({ menuOpen, setMenuOpen, menuRef }) => {
size="lg"
/>
</a>
<a
sx={{
...linkStyle,
ml: 4,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
target="__blank"
href={discourseUrl}
alt="The project's Discourse"
>
<FontAwesomeIcon
sx={{ mt: -1, path: { fill: theme.colors.text }, "&:hover": { path: { fill: theme.colors.primary } } }}
icon={faDiscourse}
size="lg"
/>
</a>
<a
sx={{
...linkStyle,

View File

@ -47,57 +47,57 @@ fontSizes.display = fontSizes[5]
// Colors
const colors = {
text: "#000",
background: "#fff",
background: "#ffffff",
primary: "#046abd",
secondary: "#6F52DA",
accent: "hsl(280, 100%, 57%)",
accent: "#b624ff",
muted: "#f9f9fc",
gray: "#555",
modes: {
black: {
text: "#fff",
text: "#ffffff",
background: "#000",
primary: "#0ff",
secondary: "#0fc",
accent: "#f0f",
muted: "#111",
gray: "#888",
primary: "#00ffff",
secondary: "#00ffcc",
accent: "#ff00ff",
muted: "#111111",
gray: "#888888",
},
dark: {
text: "#fff",
background: "hsl(180, 5%, 15%)",
primary: "hsl(180, 100%, 57%)",
secondary: "hsl(50, 100%, 57%)",
accent: "hsl(310, 100%, 57%)",
muted: "hsl(180, 5%, 5%)",
gray: "hsl(180, 0%, 70%)",
text: "#ffffff",
background: "#242828",
primary: "#24ffff",
secondary: "#ffda24",
accent: "#ff24da",
muted: "#0c0d0d",
gray: "#b3b3b3",
},
deep: {
text: "#fff",
background: "hsl(230,25%,18%)",
primary: "hsl(260, 100%, 80%)",
secondary: "hsl(290, 100%, 80%)",
accent: "hsl(290, 100%, 80%)",
muted: "hsla(230, 20%, 0%, 20%)",
gray: "hsl(210, 50%, 60%)",
text: "#ffffff",
background: "#222639",
primary: "#bb99ff",
secondary: "#ee99ff",
accent: "#ee99ff",
muted: "#000000",
gray: "#6699cc",
},
hack: {
text: "hsl(120, 100%, 75%)",
background: "hsl(120, 20%, 10%)",
primary: "hsl(120, 100%, 40%)",
secondary: "hsl(120, 50%, 40%)",
accent: "hsl(120, 100%, 90%)",
muted: "hsl(120, 20%, 7%)",
gray: "hsl(120, 20%, 40%)",
text: "#80ff80",
background: "#141f14",
primary: "#00cc00",
secondary: "#339933",
accent: "#ccffcc",
muted: "#0e150e",
gray: "#527a52",
},
pink: {
text: "hsl(350, 80%, 10%)",
background: "hsl(350, 100%, 90%)",
primary: "hsl(350, 100%, 50%)",
secondary: "hsl(280, 100%, 50%)",
accent: "hsl(280, 100%, 20%)",
muted: "hsl(350, 100%, 88%)",
gray: "hsl(350, 40%, 50%)",
text: "#2e050c",
background: "#ffccd5",
primary: "#ff002b",
secondary: "#aa00ff",
accent: "#440066",
muted: "#ffc2cc",
gray: "#b34d5e",
},
},
}
@ -112,43 +112,54 @@ const styles = {
transitionTimingFunction: "ease-out",
transitionDuration: ".4s",
},
ul: {
pl: 4
},
ol: {
pl: 4
},
a: {
color: "primary",
":hover,:focus": {
":hover,:focus,:visited": {
color: "secondary",
},
},
h1: {
variant: "text.heading",
mb: 4,
mt: 4
my: 4,
},
h2: {
mt: 4,
mb: 4,
my: 4,
variant: "text.heading",
fontSize: 4,
},
h3: {
my: 3,
my: 4,
variant: 'text.heading',
fontSize: 2,
fontSize: 3,
},
h4: {
my: 3,
variant: "text.heading",
fontSize: 2,
},
h5: {
my: 3,
variant: "text.heading",
fontSize: 2,
},
h6: {
my: 3,
variant: "text.heading",
fontSize: 2,
},
img: {
maxWidth: "100%",
height: "auto",
},
p: {
mt: 3,
},
pre: {
fontFamily: "monospace",
fontSize: 1,

View File

@ -77,7 +77,7 @@ const DocumentationPage = ({
]}
/>
<div
sx={{ display: 'flex', flexDirection: ['column', 'row'], flex: '1' }}
sx={{ display: 'flex', flexDirection: ['column', 'row'], flex: '1', overflow: 'auto' }}
>
<div
ref={ref}

View File

@ -1876,6 +1876,11 @@
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.3.1.tgz#b85c7e8fae948dc0de65aa30b853368993e25cb3"
integrity sha512-QB2/fZBpo4inaLHL3OrB8NOBgNfwnj8GtHzXWHb9iQSRjmtNX8zPXBe32jLT7qQP0+y8JxPT4YChZIkm5ZyIdg==
"@theme-ui/match-media@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@theme-ui/match-media/-/match-media-0.3.1.tgz#05af6a71cf14368e1b3bd7180fc382c72d5ba53b"
integrity sha512-PHvSRB1vqUgDnPkGlXLa+qadmOMOy3LKSOzovwpTi+wzCUyyOGAsUY/fJQ7nufBrmU3vdYeUTrKplLn5VIEmlg==
"@theme-ui/mdx@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.3.0.tgz#8bb1342204acfaa69914d6b6567c5c49d9a8c1e6"
@ -4745,6 +4750,94 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
d3-color@1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf"
integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==
d3-dispatch@1, d3-dispatch@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
d3-drag@1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70"
integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==
dependencies:
d3-dispatch "1"
d3-selection "1"
d3-ease@1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.6.tgz#ebdb6da22dfac0a22222f2d4da06f66c416a0ec0"
integrity sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ==
d3-format@^1.2.0:
version "1.4.3"
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.3.tgz#4e8eb4dff3fdcb891a8489ec6e698601c41b96f1"
integrity sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ==
d3-graphviz@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/d3-graphviz/-/d3-graphviz-2.6.1.tgz#61b93fe330e6339198fd2090f8080d7d4282c514"
integrity sha512-878AFSagQyr5tTOrM7YiVYeUC2/NoFcOB3/oew+LAML0xekyJSw9j3WOCUMBsc95KYe9XBYZ+SKKuObVya1tJQ==
dependencies:
d3-dispatch "^1.0.3"
d3-format "^1.2.0"
d3-interpolate "^1.1.5"
d3-path "^1.0.5"
d3-selection "^1.1.0"
d3-timer "^1.0.6"
d3-transition "^1.1.1"
d3-zoom "^1.5.0"
viz.js "^1.8.2"
d3-interpolate@1, d3-interpolate@^1.1.5:
version "1.4.0"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
dependencies:
d3-color "1"
d3-path@^1.0.5:
version "1.0.9"
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
d3-selection@1, d3-selection@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.1.tgz#98eedbbe085fbda5bafa2f9e3f3a2f4d7d622a98"
integrity sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA==
d3-timer@1, d3-timer@^1.0.6:
version "1.0.10"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
d3-transition@1, d3-transition@^1.1.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
dependencies:
d3-color "1"
d3-dispatch "1"
d3-ease "1"
d3-interpolate "1"
d3-selection "^1.1.0"
d3-timer "1"
d3-zoom@^1.5.0:
version "1.8.3"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a"
integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
damerau-levenshtein@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@ -6724,6 +6817,13 @@ gatsby-plugin-feed@^2.3.28:
lodash.merge "^4.6.2"
rss "^1.2.2"
gatsby-plugin-google-analytics@^2.1.38:
version "2.1.38"
resolved "https://registry.yarnpkg.com/gatsby-plugin-google-analytics/-/gatsby-plugin-google-analytics-2.1.38.tgz#37907b2000abf19dcba06da31ccca99abf3fbbc2"
integrity sha512-Ce5E1qoD1jQkDcQIm4qPmu0L66ujqwHua0Vy+UC4rUw2GdIn5dunpBklgYaDFF2DG1gDyMwJ4v+XElLaltXcDQ==
dependencies:
"@babel/runtime" "^7.8.7"
gatsby-plugin-manifest@2.2.48:
version "2.2.48"
resolved "https://registry.yarnpkg.com/gatsby-plugin-manifest/-/gatsby-plugin-manifest-2.2.48.tgz#a082addd697a9d7c27ce51f1c08690b8215c8283"
@ -6912,6 +7012,20 @@ gatsby-remark-check-links@^2.1.0:
dependencies:
unist-util-visit "^1.4.1"
gatsby-remark-copy-linked-files@^2.1.40:
version "2.1.40"
resolved "https://registry.yarnpkg.com/gatsby-remark-copy-linked-files/-/gatsby-remark-copy-linked-files-2.1.40.tgz#7c2790be10ef5a4d16b69587e5dc53f8107f7551"
integrity sha512-htZTd5rD46rg4j6KykJJE/GnV+ONidanyDlZWBJyvmIM97Jmcgh6FLpwy68PCzjw32JBdow3Wu2H//vvGYdBYw==
dependencies:
"@babel/runtime" "^7.8.7"
cheerio "^1.0.0-rc.3"
fs-extra "^8.1.0"
is-relative-url "^3.0.0"
lodash "^4.17.15"
path-is-inside "^1.0.2"
probe-image-size "^4.1.1"
unist-util-visit "^1.4.1"
gatsby-remark-images@^3.1.50:
version "3.1.50"
resolved "https://registry.yarnpkg.com/gatsby-remark-images/-/gatsby-remark-images-3.1.50.tgz#eeaba2407346b7579ab6faa63d9ecfc1a4798ea1"
@ -7541,6 +7655,14 @@ graphql@^14.6.0:
dependencies:
iterall "^1.2.2"
graphviz-react@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/graphviz-react/-/graphviz-react-1.0.4.tgz#86af1f88bf50e7ead4addc0886eb1af7404061e1"
integrity sha512-pD9BlD7By+mLL3F4MHWdZhlBJNKR9aqmX6awysQSs1+ztNcELaVbnsUn2WZmcd0c49O6EWUye3aXGw25ypSnWQ==
dependencies:
d3-graphviz "^2.6.0"
react "^16.6.0"
gray-matter@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
@ -12339,7 +12461,7 @@ react@15:
object-assign "^4.1.0"
prop-types "^15.5.10"
react@^16.13.0, react@^16.8.0, react@^16.8.6:
react@^16.13.0, react@^16.6.0, react@^16.8.0, react@^16.8.6:
version "16.13.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7"
integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ==
@ -15320,6 +15442,11 @@ vinyl@^2.2.0:
remove-trailing-separator "^1.0.1"
replace-ext "^1.0.0"
viz.js@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/viz.js/-/viz.js-1.8.2.tgz#d9cc04cd99f98ec986bf9054db76a6cbcdc5d97a"
integrity sha512-W+1+N/hdzLpQZEcvz79n2IgUE9pfx6JLdHh3Kh8RGvLL8P1LdJVQmi2OsDcLdY4QVID4OUy+FPelyerX0nJxIQ==
vm-browserify@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"