Add `ValueGraphLoader` (#2180)

* Add `ValueGraphLoader`

- Adding a new loader that load and convert models to a `ValueGraph`
- This introduces the new loader without integrating it (will be done in a separate commit)
- Updated metadata loaders to allow loading metadata needed for external dependencies in one go (equivalent to the graph node loaders)
- Added a new metadata provider for system frameworks (hosts the logic that used to be part of the deprecated SDKNode)

Test Plan:

- Verify unit tests pass

* Minor tidy ups

- Seperate types to their own dedicated files
- Move errors to the top of the file
- Add public initializers (as the types are public, they require their initalizers to be public too)
This commit is contained in:
Kas 2020-12-30 08:47:34 +00:00 committed by GitHub
parent ab9e318c9a
commit 083bd54093
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1867 additions and 57 deletions

View File

@ -5,6 +5,7 @@ import TuistSupport
enum GraphLoadingError: FatalError, Equatable {
case missingFile(AbsolutePath)
case targetNotFound(String, AbsolutePath)
case missingProject(AbsolutePath)
case manifestNotFound(AbsolutePath)
case circularDependency([GraphCircularDetectorNode])
case unexpected(String)
@ -15,6 +16,8 @@ enum GraphLoadingError: FatalError, Equatable {
return lhsPath == rhsPath
case let (.targetNotFound(lhsName, lhsPath), .targetNotFound(rhsName, rhsPath)):
return lhsPath == rhsPath && lhsName == rhsName
case let (.missingProject(lhsPath), .missingProject(rhsPath)):
return lhsPath == rhsPath
case let (.manifestNotFound(lhsPath), .manifestNotFound(rhsPath)):
return lhsPath == rhsPath
case let (.unexpected(lhsMessage), .unexpected(rhsMessage)):
@ -36,6 +39,8 @@ enum GraphLoadingError: FatalError, Equatable {
return "Couldn't find manifest at path: '\(path.pathString)'"
case let .targetNotFound(targetName, path):
return "Couldn't find target '\(targetName)' at '\(path.pathString)'"
case let .missingProject(path):
return "Could not locate project at path: \(path.pathString)"
case let .missingFile(path):
return "Couldn't find file at path '\(path.pathString)'"
case let .unexpected(message):

View File

@ -2,27 +2,12 @@ import Foundation
import TSCBasic
import TuistSupport
public enum SDKSource {
case developer // Platforms/iPhoneOS.platform/Developer/Library
case system // Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library
/// Returns the framewok search path that should be used in Xcode to locate the SDK.
public var frameworkSearchPath: String? {
switch self {
case .developer:
return "$(PLATFORM_DIR)/Developer/Library/Frameworks"
case .system:
return nil
}
}
}
@available(*, deprecated, message: "SDK nodes are deprecated. Dependencies should be usted instead with the ValueGraph.")
public class SDKNode: GraphNode {
static let xctestFrameworkName = "XCTest.framework"
public let status: SDKStatus
public let type: Type
public let type: SDKType
public let source: SDKSource
public init(name: String,
@ -32,7 +17,7 @@ public class SDKNode: GraphNode {
{
let sdk = AbsolutePath("/\(name)")
// TODO: Validate using a linter
guard let sdkExtension = sdk.extension, let type = Type(rawValue: sdkExtension) else {
guard let sdkExtension = sdk.extension, let type = SDKType(rawValue: sdkExtension) else {
throw Error.unsupported(sdk: name)
}
self.status = status
@ -60,7 +45,7 @@ public class SDKNode: GraphNode {
try SDKNode(name: "AppClip.framework", platform: .iOS, status: status, source: .system)
}
static func path(name: String, platform: Platform, source _: SDKSource, type: Type) throws -> AbsolutePath {
static func path(name: String, platform: Platform, source _: SDKSource, type: SDKType) throws -> AbsolutePath {
let sdkRootPath: AbsolutePath
if name == SDKNode.xctestFrameworkName {
guard let xcodeDeveloperSdkRootPath = platform.xcodeDeveloperSdkRootPath else {
@ -87,24 +72,12 @@ public class SDKNode: GraphNode {
}
}
public enum `Type`: String, CaseIterable {
case framework
case library = "tbd"
static var supportedTypesDescription: String {
let supportedTypes = allCases
.map { ".\($0.rawValue)" }
.joined(separator: ", ")
return "[\(supportedTypes)]"
}
}
enum Error: FatalError, Equatable {
case unsupported(sdk: String)
var description: String {
switch self {
case let .unsupported(sdk):
let supportedTypes = Type.supportedTypesDescription
let supportedTypes = SDKType.supportedTypesDescription
return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported."
}
}

View File

@ -2,7 +2,35 @@ import Foundation
import TSCBasic
import TuistSupport
// MARK: - Provider Errors
enum FrameworkMetadataProviderError: FatalError, Equatable {
case frameworkNotFound(AbsolutePath)
// MARK: - FatalError
var description: String {
switch self {
case let .frameworkNotFound(path):
return "Couldn't find framework at \(path.pathString)"
}
}
var type: ErrorType {
switch self {
case .frameworkNotFound:
return .abort
}
}
}
// MARK: - Provider
public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
/// Loads all the metadata associated with a framework at the specified path
/// - Note: This performs various shell calls and disk operations
func loadMetadata(at path: AbsolutePath) throws -> FrameworkMetadata
/// 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.
@ -18,7 +46,35 @@ public protocol FrameworkMetadataProviding: PrecompiledMetadataProviding {
func product(frameworkPath: AbsolutePath) throws -> Product
}
// MARK: - Default Implementation
public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, FrameworkMetadataProviding {
override public init() {
super.init()
}
public func loadMetadata(at path: AbsolutePath) throws -> FrameworkMetadata {
let fileHandler = FileHandler.shared
guard fileHandler.exists(path) else {
throw FrameworkMetadataProviderError.frameworkNotFound(path)
}
let binaryPath = self.binaryPath(frameworkPath: path)
let dsymPath = self.dsymPath(frameworkPath: path)
let bcsymbolmapPaths = try self.bcsymbolmapPaths(frameworkPath: path)
let linking = try self.linking(binaryPath: binaryPath)
let architectures = try self.architectures(binaryPath: binaryPath)
let isCarthage = path.pathString.contains("Carthage/Build")
return FrameworkMetadata(
path: path,
binaryPath: binaryPath,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures,
isCarthage: isCarthage
)
}
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
let path = AbsolutePath("\(frameworkPath.pathString).dSYM")
if FileHandler.shared.exists(path) { return path }
@ -26,7 +82,7 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame
}
public func bcsymbolmapPaths(frameworkPath: AbsolutePath) throws -> [AbsolutePath] {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
let binaryPath = self.binaryPath(frameworkPath: frameworkPath)
let uuids = try self.uuids(binaryPath: binaryPath)
return uuids
.map { frameworkPath.parentDirectory.appending(component: "\($0).bcsymbolmap") }
@ -35,7 +91,7 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame
}
public func product(frameworkPath: AbsolutePath) throws -> Product {
let binaryPath = FrameworkNode.binaryPath(frameworkPath: frameworkPath)
let binaryPath = self.binaryPath(frameworkPath: frameworkPath)
switch try linking(binaryPath: binaryPath) {
case .dynamic:
return .framework
@ -43,4 +99,8 @@ public final class FrameworkMetadataProvider: PrecompiledMetadataProvider, Frame
return .staticFramework
}
}
private func binaryPath(frameworkPath: AbsolutePath) -> AbsolutePath {
frameworkPath.appending(component: frameworkPath.basenameWithoutExt)
}
}

View File

@ -2,19 +2,76 @@ import Foundation
import TSCBasic
import TuistSupport
protocol LibraryMetadataProviding: PrecompiledMetadataProviding {
/// Returns the product for the given library.
/// - Parameter library: Library instance.
func product(library: LibraryNode) throws -> Product
}
// MARK: - Provider Errors
final class LibraryMetadataProvider: PrecompiledMetadataProvider, LibraryMetadataProviding {
func product(library: LibraryNode) throws -> Product {
switch try linking(binaryPath: library.path) {
case .dynamic:
return .dynamicLibrary
case .static:
return .staticLibrary
enum LibraryMetadataProviderError: FatalError, Equatable {
case libraryNotFound(AbsolutePath)
case publicHeadersNotFound(libraryPath: AbsolutePath, headersPath: AbsolutePath)
case swiftModuleMapNotFound(libraryPath: AbsolutePath, moduleMapPath: AbsolutePath)
// MARK: - FatalError
var description: String {
switch self {
case let .libraryNotFound(path):
return "Couldn't find library at \(path.pathString)"
case let .publicHeadersNotFound(libraryPath: libraryPath, headersPath: headersPath):
return "Couldn't find the public headers at \(headersPath.pathString) for library \(libraryPath.pathString)"
case let .swiftModuleMapNotFound(libraryPath: libraryPath, moduleMapPath: moduleMapPath):
return "Couldn't find the public headers at \(moduleMapPath.pathString) for library \(libraryPath.pathString)"
}
}
var type: ErrorType {
switch self {
case .libraryNotFound, .publicHeadersNotFound, .swiftModuleMapNotFound:
return .abort
}
}
}
// MARK: - Provider
public protocol LibraryMetadataProviding: PrecompiledMetadataProviding {
/// Loads all the metadata associated with a library (.a / .dylib) at the specified path
/// - Note: This performs various shell calls and disk operations
func loadMetadata(at path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?) throws -> LibraryMetadata
}
// MARK: - Default Implementation
public final class LibraryMetadataProvider: PrecompiledMetadataProvider, LibraryMetadataProviding {
override public init() {
super.init()
}
public func loadMetadata(at path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?) throws -> LibraryMetadata
{
let fileHandler = FileHandler.shared
guard fileHandler.exists(path) else {
throw LibraryMetadataProviderError.libraryNotFound(path)
}
guard fileHandler.exists(publicHeaders) else {
throw LibraryMetadataProviderError.publicHeadersNotFound(libraryPath: path, headersPath: publicHeaders)
}
if let swiftModuleMap = swiftModuleMap {
guard fileHandler.exists(swiftModuleMap) else {
throw LibraryMetadataProviderError.swiftModuleMapNotFound(libraryPath: path, moduleMapPath: swiftModuleMap)
}
}
let architectures = try self.architectures(binaryPath: path)
let linking = try self.linking(binaryPath: path)
return LibraryMetadata(
path: path,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap,
architectures: architectures,
linking: linking
)
}
}

View File

@ -47,8 +47,6 @@ public protocol PrecompiledMetadataProviding {
}
public class PrecompiledMetadataProvider: PrecompiledMetadataProviding {
public init() {}
public func architectures(binaryPath: AbsolutePath) throws -> [BinaryArchitecture] {
let result = try System.shared.capture("/usr/bin/lipo", "-info", binaryPath.pathString).spm_chuzzle() ?? ""
let regexes = [

View File

@ -0,0 +1,87 @@
import Foundation
import TSCBasic
import TuistSupport
// MARK: - Provider Errors
public enum SystemFrameworkMetadataProviderError: FatalError, Equatable {
case unsupportedSDK(name: String)
case unsupportedSDKForPlatform(name: String, platform: Platform)
public var description: String {
switch self {
case let .unsupportedSDK(sdk):
let supportedTypes = SDKType.supportedTypesDescription
return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported."
case let .unsupportedSDKForPlatform(name: sdk, platform: platform):
return "The SDK \(sdk) is not currently supported on \(platform)."
}
}
public var type: ErrorType {
switch self {
case .unsupportedSDK, .unsupportedSDKForPlatform:
return .abort
}
}
}
// MARK: - Provider
public protocol SystemFrameworkMetadataProviding {
func loadMetadata(sdkName: String, status: SDKStatus, platform: Platform, source: SDKSource) throws -> SystemFrameworkMetadata
}
extension SystemFrameworkMetadataProviding {
func loadXCTestMetadata(platform: Platform) throws -> SystemFrameworkMetadata {
try loadMetadata(sdkName: "XCTest.framework", status: .required, platform: platform, source: .developer)
}
}
// MARK: - Default Implementation
public final class SystemFrameworkMetadataProvider: SystemFrameworkMetadataProviding {
public init() {}
public func loadMetadata(sdkName: String, status: SDKStatus, platform: Platform, source: SDKSource) throws -> SystemFrameworkMetadata {
let sdkNamePath = AbsolutePath("/\(sdkName)")
guard let sdkExtension = sdkNamePath.extension,
let sdkType = SDKType(rawValue: sdkExtension)
else {
throw SystemFrameworkMetadataProviderError.unsupportedSDK(name: sdkName)
}
let path = try sdkPath(name: sdkName, platform: platform, type: sdkType, source: source)
return SystemFrameworkMetadata(
name: sdkName,
path: path,
status: status,
source: source
)
}
private func sdkPath(name: String, platform: Platform, type: SDKType, source: SDKSource) throws -> AbsolutePath {
switch source {
case .developer:
guard let xcodeDeveloperSdkRootPath = platform.xcodeDeveloperSdkRootPath else {
throw SystemFrameworkMetadataProviderError.unsupportedSDKForPlatform(name: name, platform: platform)
}
let sdkRootPath = AbsolutePath("/\(xcodeDeveloperSdkRootPath)")
return sdkRootPath
.appending(RelativePath("Frameworks"))
.appending(component: name)
case .system:
let sdkRootPath = AbsolutePath("/\(platform.xcodeSdkRootPath)")
switch type {
case .framework:
return sdkRootPath
.appending(RelativePath("System/Library/Frameworks"))
.appending(component: name)
case .library:
return sdkRootPath
.appending(RelativePath("usr/lib"))
.appending(component: name)
}
}
}
}

View File

@ -2,7 +2,10 @@ import Foundation
import TSCBasic
import TuistSupport
// MARK: - Provider Errors
enum XCFrameworkMetadataProviderError: FatalError, Equatable {
case xcframeworkNotFound(AbsolutePath)
case missingRequiredFile(AbsolutePath)
case supportedArchitectureReferencesNotFound(AbsolutePath)
case fileTypeNotRecognised(file: RelativePath, frameworkName: String)
@ -11,6 +14,8 @@ enum XCFrameworkMetadataProviderError: FatalError, Equatable {
var description: String {
switch self {
case let .xcframeworkNotFound(path):
return "Couldn't find xcframework at \(path.pathString)"
case let .missingRequiredFile(path):
return "The .xcframework at path \(path.pathString) doesn't contain an Info.plist. It's possible that the .xcframework was not generated properly or that got corrupted. Please, double check with the author of the framework."
case let .supportedArchitectureReferencesNotFound(path):
@ -22,13 +27,19 @@ enum XCFrameworkMetadataProviderError: FatalError, Equatable {
var type: ErrorType {
switch self {
case .missingRequiredFile, .supportedArchitectureReferencesNotFound, .fileTypeNotRecognised:
case .xcframeworkNotFound, .missingRequiredFile, .supportedArchitectureReferencesNotFound, .fileTypeNotRecognised:
return .abort
}
}
}
protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding {
// MARK: - Provider
public protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding {
/// Loads all the metadata associated with an XCFramework at the specified path
/// - Note: This performs various shell calls and disk operations
func loadMetadata(at path: AbsolutePath) throws -> XCFrameworkMetadata
/// Returns the info.plist of the xcframework at the given path.
/// - Parameter xcframeworkPath: Path to the xcframework.
func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist
@ -39,8 +50,33 @@ protocol XCFrameworkMetadataProviding: PrecompiledMetadataProviding {
func binaryPath(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath
}
class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCFrameworkMetadataProviding {
func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist {
// MARK: - Default Implementation
public final class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCFrameworkMetadataProviding {
override public init() {
super.init()
}
public func loadMetadata(at path: AbsolutePath) throws -> XCFrameworkMetadata {
let fileHandler = FileHandler.shared
guard fileHandler.exists(path) else {
throw XCFrameworkMetadataProviderError.xcframeworkNotFound(path)
}
let infoPlist = try self.infoPlist(xcframeworkPath: path)
let primaryBinaryPath = try binaryPath(
xcframeworkPath: path,
libraries: infoPlist.libraries
)
let linking = try self.linking(binaryPath: primaryBinaryPath)
return XCFrameworkMetadata(
path: path,
infoPlist: infoPlist,
primaryBinaryPath: primaryBinaryPath,
linking: linking
)
}
public func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist {
let fileHandler = FileHandler.shared
let infoPlist = xcframeworkPath.appending(component: "Info.plist")
guard fileHandler.exists(infoPlist) else {
@ -50,7 +86,7 @@ class XCFrameworkMetadataProvider: PrecompiledMetadataProvider, XCFrameworkMetad
return try fileHandler.readPlistFile(infoPlist)
}
func binaryPath(xcframeworkPath: AbsolutePath, libraries: [XCFrameworkInfoPlist.Library]) throws -> AbsolutePath {
public 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(xcframeworkPath)

View File

@ -0,0 +1,31 @@
import Foundation
import TSCBasic
/// The metadata associated with a precompiled framework (.framework)
public struct FrameworkMetadata: Equatable {
public var path: AbsolutePath
public var binaryPath: AbsolutePath
public var dsymPath: AbsolutePath?
public var bcsymbolmapPaths: [AbsolutePath]
public var linking: BinaryLinking
public var architectures: [BinaryArchitecture]
public var isCarthage: Bool
public init(
path: AbsolutePath,
binaryPath: AbsolutePath,
dsymPath: AbsolutePath?,
bcsymbolmapPaths: [AbsolutePath],
linking: BinaryLinking,
architectures: [BinaryArchitecture],
isCarthage: Bool
) {
self.path = path
self.binaryPath = binaryPath
self.dsymPath = dsymPath
self.bcsymbolmapPaths = bcsymbolmapPaths
self.linking = linking
self.architectures = architectures
self.isCarthage = isCarthage
}
}

View File

@ -0,0 +1,25 @@
import Foundation
import TSCBasic
/// The metadata associated with a precompiled library (.a / .dylib)
public struct LibraryMetadata: Equatable {
public var path: AbsolutePath
public var publicHeaders: AbsolutePath
public var swiftModuleMap: AbsolutePath?
public var architectures: [BinaryArchitecture]
public var linking: BinaryLinking
public init(
path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?,
architectures: [BinaryArchitecture],
linking: BinaryLinking
) {
self.path = path
self.publicHeaders = publicHeaders
self.swiftModuleMap = swiftModuleMap
self.architectures = architectures
self.linking = linking
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import TSCBasic
/// The metadata associated with a system framework or library (e.g. UIKit.framework, libc++.tbd)
public struct SystemFrameworkMetadata: Equatable {
var name: String
var path: AbsolutePath
var status: SDKStatus
var source: SDKSource
public init(
name: String,
path: AbsolutePath,
status: SDKStatus,
source: SDKSource
) {
self.name = name
self.path = path
self.status = status
self.source = source
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import TSCBasic
/// The metadata associated with a precompiled xcframework
public struct XCFrameworkMetadata: Equatable {
public var path: AbsolutePath
public var infoPlist: XCFrameworkInfoPlist
public var primaryBinaryPath: AbsolutePath
public var linking: BinaryLinking
public init(
path: AbsolutePath,
infoPlist: XCFrameworkInfoPlist,
primaryBinaryPath: AbsolutePath,
linking: BinaryLinking
) {
self.path = path
self.infoPlist = infoPlist
self.primaryBinaryPath = primaryBinaryPath
self.linking = linking
}
}

View File

@ -0,0 +1,16 @@
import Foundation
public enum SDKSource: Equatable {
case developer // Platforms/iPhoneOS.platform/Developer/Library
case system // Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library
/// Returns the framework search path that should be used in Xcode to locate the SDK.
public var frameworkSearchPath: String? {
switch self {
case .developer:
return "$(PLATFORM_DIR)/Developer/Library/Frameworks"
case .system:
return nil
}
}
}

View File

@ -0,0 +1,13 @@
import Foundation
public enum SDKType: String, CaseIterable, Equatable {
case framework
case library = "tbd"
static var supportedTypesDescription: String {
let supportedTypes = allCases
.map { ".\($0.rawValue)" }
.joined(separator: ", ")
return "[\(supportedTypes)]"
}
}

View File

@ -0,0 +1,365 @@
import Foundation
import TSCBasic
import TuistSupport
public protocol ValueGraphLoading {
func loadWorkspace(workspace: Workspace, projects: [Project]) throws -> ValueGraph
func loadProject(at path: AbsolutePath, projects: [Project]) throws -> (Project, ValueGraph)
}
// MARK: - ValueGraphLoader
public final class ValueGraphLoader: ValueGraphLoading {
private let frameworkMetadataProvider: FrameworkMetadataProviding
private let libraryMetadataProvider: LibraryMetadataProviding
private let xcframeworkMetadataProvider: XCFrameworkMetadataProviding
private let systemFrameworkMetadataProvider: SystemFrameworkMetadataProviding
public convenience init() {
self.init(
frameworkMetadataProvider: FrameworkMetadataProvider(),
libraryMetadataProvider: LibraryMetadataProvider(),
xcframeworkMetadataProvider: XCFrameworkMetadataProvider(),
systemFrameworkMetadataProvider: SystemFrameworkMetadataProvider()
)
}
public init(
frameworkMetadataProvider: FrameworkMetadataProviding,
libraryMetadataProvider: LibraryMetadataProviding,
xcframeworkMetadataProvider: XCFrameworkMetadataProviding,
systemFrameworkMetadataProvider: SystemFrameworkMetadataProviding
) {
self.frameworkMetadataProvider = frameworkMetadataProvider
self.libraryMetadataProvider = libraryMetadataProvider
self.xcframeworkMetadataProvider = xcframeworkMetadataProvider
self.systemFrameworkMetadataProvider = systemFrameworkMetadataProvider
}
// MARK: - ValueGraphLoading
public func loadWorkspace(workspace: Workspace, projects: [Project]) throws -> ValueGraph {
let cache = Cache(projects: projects)
let cycleDetector = GraphCircularDetector()
try workspace.projects.forEach { project in
try loadProject(
path: project,
cache: cache,
cycleDetector: cycleDetector
)
}
let updatedWorkspace = workspace.replacing(projects: cache.loadedProjects.keys.sorted())
let graph = ValueGraph(
name: updatedWorkspace.name,
path: updatedWorkspace.path,
workspace: updatedWorkspace,
projects: cache.loadedProjects,
packages: cache.packages,
targets: cache.loadedTargets,
dependencies: cache.dependencies
)
return graph
}
public func loadProject(at path: AbsolutePath, projects: [Project]) throws -> (Project, ValueGraph) {
let cache = Cache(projects: projects)
guard let rootProject = cache.allProjects[path] else {
throw GraphLoadingError.missingProject(path)
}
let cycleDetector = GraphCircularDetector()
try loadProject(path: path, cache: cache, cycleDetector: cycleDetector)
let workspace = Workspace(
path: path,
name: rootProject.name,
projects: cache.loadedProjects.keys.sorted()
)
let graph = ValueGraph(
name: rootProject.name,
path: path,
workspace: workspace,
projects: cache.loadedProjects,
packages: cache.packages,
targets: cache.loadedTargets,
dependencies: cache.dependencies
)
return (rootProject, graph)
}
// MARK: - Private
private func loadProject(
path: AbsolutePath,
cache: Cache,
cycleDetector: GraphCircularDetector
) throws {
guard !cache.projectLoaded(path: path) else {
return
}
guard let project = cache.allProjects[path] else {
throw GraphLoadingError.missingProject(path)
}
cache.add(project: project)
try project.targets.forEach {
try loadTarget(
path: path,
name: $0.name,
cache: cache,
cycleDetector: cycleDetector
)
}
}
private func loadTarget(
path: AbsolutePath,
name: String,
cache: Cache,
cycleDetector: GraphCircularDetector
) throws {
guard !cache.targetLoaded(path: path, name: name) else {
return
}
guard let _ = cache.allProjects[path] else {
throw GraphLoadingError.missingProject(path)
}
guard let referencedTargetProject = cache.allTargets[path],
let target = referencedTargetProject[name]
else {
throw GraphLoadingError.targetNotFound(name, path)
}
cache.add(target: target, path: path)
let dependencies = try target.dependencies.map {
try loadDependency(
path: path,
fromTarget: target.name,
fromPlatform: target.platform,
dependency: $0,
cache: cache,
cycleDetector: cycleDetector
)
}
try cycleDetector.complete()
if !dependencies.isEmpty {
cache.dependencies[.target(name: name, path: path)] = Set(dependencies)
}
}
private func loadDependency(
path: AbsolutePath,
fromTarget: String,
fromPlatform: Platform,
dependency: Dependency,
cache: Cache,
cycleDetector: GraphCircularDetector
) throws -> ValueGraphDependency {
switch dependency {
case let .target(toTarget):
// A target within the same project.
let circularFrom = GraphCircularDetectorNode(path: path, name: fromTarget)
let circularTo = GraphCircularDetectorNode(path: path, name: toTarget)
cycleDetector.start(from: circularFrom, to: circularTo)
try loadTarget(
path: path,
name: toTarget,
cache: cache,
cycleDetector: cycleDetector
)
return .target(name: toTarget, path: path)
case let .project(toTarget, projectPath):
// A target from another project
let circularFrom = GraphCircularDetectorNode(path: path, name: fromTarget)
let circularTo = GraphCircularDetectorNode(path: projectPath, name: toTarget)
cycleDetector.start(from: circularFrom, to: circularTo)
try loadProject(path: projectPath, cache: cache, cycleDetector: cycleDetector)
try loadTarget(
path: projectPath,
name: toTarget,
cache: cache,
cycleDetector: cycleDetector
)
return .target(name: toTarget, path: projectPath)
case let .framework(frameworkPath):
return try loadFramework(path: frameworkPath, cache: cache)
case let .library(libraryPath, publicHeaders, swiftModuleMap):
return try loadLibrary(
path: libraryPath,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap,
cache: cache
)
case let .xcFramework(frameworkPath):
return try loadXCFramework(path: frameworkPath, cache: cache)
case let .sdk(name, status):
return try loadSDK(name: name, platform: fromPlatform, status: status, source: .system)
case let .cocoapods(podsPath):
return .cocoapods(path: podsPath)
case let .package(product):
return try loadPackage(fromPath: path, productName: product)
case .xctest:
return try loadXCTestSDK(platform: fromPlatform)
}
}
private func loadFramework(path: AbsolutePath, cache: Cache) throws -> ValueGraphDependency {
if let loaded = cache.frameworks[path] {
return loaded
}
let metadata = try frameworkMetadataProvider.loadMetadata(at: path)
let framework: ValueGraphDependency = .framework(
path: metadata.path,
binaryPath: metadata.binaryPath,
dsymPath: metadata.dsymPath,
bcsymbolmapPaths: metadata.bcsymbolmapPaths,
linking: metadata.linking,
architectures: metadata.architectures,
isCarthage: metadata.isCarthage
)
cache.add(framework: framework, at: path)
return framework
}
private func loadLibrary(
path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?,
cache: Cache
) throws -> ValueGraphDependency {
if let loaded = cache.libraries[path] {
return loaded
}
let metadata = try libraryMetadataProvider.loadMetadata(
at: path,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap
)
let library: ValueGraphDependency = .library(
path: metadata.path,
publicHeaders: metadata.publicHeaders,
linking: metadata.linking,
architectures: metadata.architectures,
swiftModuleMap: metadata.swiftModuleMap
)
cache.add(library: library, at: path)
return library
}
private func loadXCFramework(path: AbsolutePath, cache: Cache) throws -> ValueGraphDependency {
if let loaded = cache.xcframeworks[path] {
return loaded
}
let metadata = try xcframeworkMetadataProvider.loadMetadata(at: path)
let xcframework: ValueGraphDependency = .xcframework(
path: metadata.path,
infoPlist: metadata.infoPlist,
primaryBinaryPath: metadata.primaryBinaryPath,
linking: metadata.linking
)
cache.add(xcframework: xcframework, at: path)
return xcframework
}
private func loadSDK(name: String,
platform: Platform,
status: SDKStatus,
source: SDKSource) throws -> ValueGraphDependency
{
let metadata = try systemFrameworkMetadataProvider.loadMetadata(sdkName: name, status: status, platform: platform, source: source)
return .sdk(name: metadata.name, path: metadata.path, status: metadata.status, source: metadata.source)
}
private func loadXCTestSDK(platform: Platform) throws -> ValueGraphDependency {
let metadata = try systemFrameworkMetadataProvider.loadXCTestMetadata(platform: platform)
return .sdk(name: metadata.name, path: metadata.path, status: metadata.status, source: metadata.source)
}
private func loadPackage(fromPath: AbsolutePath, productName: String) throws -> ValueGraphDependency {
// TODO: `fromPath` isn't quite correct as it reflects the path where the dependency was declared
// and doesn't uniquely identify it. It's been copied from the previous implementation to maintain
// existing behaviour and should be fixed separately
.packageProduct(
path: fromPath,
product: productName
)
}
private final class Cache {
let allProjects: [AbsolutePath: Project]
let allTargets: [AbsolutePath: [String: Target]]
var loadedProjects: [AbsolutePath: Project] = [:]
var loadedTargets: [AbsolutePath: [String: Target]] = [:]
var dependencies: [ValueGraphDependency: Set<ValueGraphDependency>] = [:]
var frameworks: [AbsolutePath: ValueGraphDependency] = [:]
var libraries: [AbsolutePath: ValueGraphDependency] = [:]
var xcframeworks: [AbsolutePath: ValueGraphDependency] = [:]
var packages: [AbsolutePath: [String: Package]] = [:]
init(projects: [Project]) {
let allProjects = Dictionary(uniqueKeysWithValues: projects.map { ($0.path, $0) })
let allTargets = allProjects.mapValues {
Dictionary(uniqueKeysWithValues: $0.targets.map { ($0.name, $0) })
}
self.allProjects = allProjects
self.allTargets = allTargets
}
func add(project: Project) {
loadedProjects[project.path] = project
project.packages.forEach {
packages[project.path, default: [:]][$0.name] = $0
}
}
func add(target: Target, path: AbsolutePath) {
loadedTargets[path, default: [:]][target.name] = target
}
func add(framework: ValueGraphDependency, at path: AbsolutePath) {
frameworks[path] = framework
}
func add(xcframework: ValueGraphDependency, at path: AbsolutePath) {
xcframeworks[path] = xcframework
}
func add(library: ValueGraphDependency, at path: AbsolutePath) {
libraries[path] = library
}
func targetLoaded(path: AbsolutePath, name: String) -> Bool {
loadedTargets[path]?[name] != nil
}
func projectLoaded(path: AbsolutePath) -> Bool {
loadedProjects[path] != nil
}
}
}
private extension Package {
var name: String {
switch self {
case let .local(path: path):
return path.pathString
case let .remote(url: url, requirement: _):
return url
}
}
}

View File

@ -3,6 +3,15 @@ import TSCBasic
@testable import TuistCore
public final class MockFrameworkMetadataProvider: MockPrecompiledMetadataProvider, FrameworkMetadataProviding {
public var loadMetadataStub: ((AbsolutePath) throws -> FrameworkMetadata)?
public func loadMetadata(at path: AbsolutePath) throws -> FrameworkMetadata {
if let loadMetadataStub = loadMetadataStub {
return try loadMetadataStub(path)
} else {
return FrameworkMetadata.test(path: path)
}
}
public var dsymPathStub: ((AbsolutePath) -> AbsolutePath?)?
public func dsymPath(frameworkPath: AbsolutePath) -> AbsolutePath? {
dsymPathStub?(frameworkPath) ?? nil

View File

@ -3,12 +3,19 @@ import TSCBasic
@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)
public var loadMetadataStub: ((AbsolutePath, AbsolutePath, AbsolutePath?) throws -> LibraryMetadata)?
public func loadMetadata(at path: AbsolutePath,
publicHeaders: AbsolutePath,
swiftModuleMap: AbsolutePath?) throws -> LibraryMetadata
{
if let stub = loadMetadataStub {
return try stub(path, publicHeaders, swiftModuleMap)
} else {
return .staticLibrary
return LibraryMetadata.test(
path: path,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap
)
}
}
}

View File

@ -3,6 +3,18 @@ import TSCBasic
@testable import TuistCore
public final class MockXCFrameworkMetadataProvider: MockPrecompiledMetadataProvider, XCFrameworkMetadataProviding {
public var loadMetadataStub: ((AbsolutePath) throws -> XCFrameworkMetadata)?
public func loadMetadata(at path: AbsolutePath) throws -> XCFrameworkMetadata {
if let loadMetadataStub = loadMetadataStub {
return try loadMetadataStub(path)
} else {
return XCFrameworkMetadata.test(
path: path,
primaryBinaryPath: path.appending(RelativePath("ios-arm64/binary"))
)
}
}
public var infoPlistStub: ((AbsolutePath) throws -> XCFrameworkInfoPlist)?
public func infoPlist(xcframeworkPath: AbsolutePath) throws -> XCFrameworkInfoPlist {
if let infoPlistStub = infoPlistStub {

View File

@ -0,0 +1,32 @@
//
// File.swift
//
//
// Created by Kassem Wridan on 22/12/2020.
//
import Foundation
import TSCBasic
import TuistCore
public extension FrameworkMetadata {
static func test(
path: AbsolutePath = "/Frameworks/TestFramework.xframework",
binaryPath: AbsolutePath = "/Frameworks/TestFramework.xframework/TestFramework",
dsymPath: AbsolutePath? = nil,
bcsymbolmapPaths: [AbsolutePath] = [],
linking: BinaryLinking = .dynamic,
architectures: [BinaryArchitecture] = [.arm64],
isCarthage: Bool = false
) -> FrameworkMetadata {
FrameworkMetadata(
path: path,
binaryPath: binaryPath,
dsymPath: dsymPath,
bcsymbolmapPaths: bcsymbolmapPaths,
linking: linking,
architectures: architectures,
isCarthage: isCarthage
)
}
}

View File

@ -0,0 +1,28 @@
//
// File.swift
//
//
// Created by Kassem Wridan on 22/12/2020.
//
import Foundation
import TSCBasic
import TuistCore
public extension LibraryMetadata {
static func test(
path: AbsolutePath = "/Libraries/libTest/libTest.a",
publicHeaders: AbsolutePath = "/Libraries/libTest/include",
swiftModuleMap: AbsolutePath? = "/Libraries/libTest/libTest.swiftmodule",
architectures: [BinaryArchitecture] = [.arm64],
linking: BinaryLinking = .static
) -> LibraryMetadata {
LibraryMetadata(
path: path,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap,
architectures: architectures,
linking: linking
)
}
}

View File

@ -0,0 +1,19 @@
import Foundation
import TSCBasic
import TuistCore
public extension XCFrameworkMetadata {
static func test(
path: AbsolutePath = "/XCFrameworks/XCFramework.xcframework",
infoPlist: XCFrameworkInfoPlist = .test(),
primaryBinaryPath: AbsolutePath = "/XCFrameworks/XCFramework.xcframework/ios-arm64/XCFramework",
linking: BinaryLinking = .dynamic
) -> XCFrameworkMetadata {
XCFrameworkMetadata(
path: path,
infoPlist: infoPlist,
primaryBinaryPath: primaryBinaryPath,
linking: linking
)
}
}

View File

@ -0,0 +1,40 @@
import TSCBasic
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class FrameworkMetadataProviderTests: XCTestCase {
var subject: FrameworkMetadataProvider!
override func setUp() {
super.setUp()
subject = FrameworkMetadataProvider()
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_loadMetadata() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("xpm.framework"))
// When
let metadata = try subject.loadMetadata(at: frameworkPath)
// Then
let expectedBinaryPath = frameworkPath.appending(component: frameworkPath.basenameWithoutExt)
let expectedDsymPath = frameworkPath.parentDirectory.appending(component: "xpm.framework.dSYM")
XCTAssertEqual(metadata, FrameworkMetadata(
path: frameworkPath,
binaryPath: expectedBinaryPath,
dsymPath: expectedDsymPath,
bcsymbolmapPaths: [],
linking: .dynamic,
architectures: [.x8664, .arm64],
isCarthage: false
))
}
}

View File

@ -0,0 +1,36 @@
import TSCBasic
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class LibraryMetadataProviderTests: XCTestCase {
var subject: LibraryMetadataProvider!
override func setUp() {
super.setUp()
subject = LibraryMetadataProvider()
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_loadMetadata() throws {
// Given
let libraryPath = fixturePath(path: RelativePath("libStaticLibrary.a"))
// When
let metadata = try subject.loadMetadata(at: libraryPath, publicHeaders: libraryPath.parentDirectory, swiftModuleMap: nil)
// Then
XCTAssertEqual(metadata, LibraryMetadata(
path: libraryPath,
publicHeaders: libraryPath.parentDirectory,
swiftModuleMap: nil,
architectures: [.x8664],
linking: .static
))
}
}

View File

@ -0,0 +1,93 @@
import TSCBasic
import XCTest
@testable import TuistCore
@testable import TuistSupportTesting
final class SystemFrameworkMetadataProviderTests: XCTestCase {
var subject: SystemFrameworkMetadataProvider!
override func setUp() {
super.setUp()
subject = SystemFrameworkMetadataProvider()
}
override func tearDown() {
subject = nil
super.tearDown()
}
func test_loadMetadata_framework() throws {
// Given
let sdkName = "UIKit.framework"
// When
let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system)
// Then
XCTAssertEqual(metadata, SystemFrameworkMetadata(
name: sdkName,
path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/UIKit.framework",
status: .required,
source: .system
))
}
func test_loadMetadata_library() throws {
// Given
let sdkName = "libc++.tbd"
// When
let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system)
// Then
XCTAssertEqual(metadata, SystemFrameworkMetadata(
name: sdkName,
path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/libc++.tbd",
status: .required,
source: .system
))
}
func test_loadMetadata_unsupportedType() throws {
// Given
let sdkName = "UIKit.xcframework"
// When / Then
XCTAssertThrowsSpecific(
try subject.loadMetadata(sdkName: sdkName, status: .required, platform: .iOS, source: .system),
SystemFrameworkMetadataProviderError.unsupportedSDK(name: "UIKit.xcframework")
)
}
func test_loadMetadata_developerSource_unsupportedPlatform() throws {
// Given
let sdkName = "XCTest.framework"
let source = SDKSource.developer
let platform = Platform.watchOS // watchOS doesn't support XCTest
// When / Then
XCTAssertThrowsSpecific(
try subject.loadMetadata(sdkName: sdkName, status: .required, platform: platform, source: source),
SystemFrameworkMetadataProviderError.unsupportedSDKForPlatform(name: "XCTest.framework", platform: .watchOS)
)
}
func test_loadMetadata_developerSource_supportedPlatform() throws {
// Given
let sdkName = "XCTest.framework"
let source = SDKSource.developer
let platform = Platform.iOS
// When
let metadata = try subject.loadMetadata(sdkName: sdkName, status: .required, platform: platform, source: source)
// Then
XCTAssertEqual(metadata, SystemFrameworkMetadata(
name: sdkName,
path: "/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework",
status: .required,
source: .developer
))
}
}

View File

@ -74,4 +74,62 @@ final class XCFrameworkMetadataProviderTests: XCTestCase {
frameworkPath.appending(RelativePath("ios-x86_64-simulator/libMyStaticLibrary.a"))
)
}
func test_loadMetadata_dynamicLibrary() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyFramework.xcframework"))
// When
let metadata = try subject.loadMetadata(at: frameworkPath)
// Then
let expectedInfoPlist = XCFrameworkInfoPlist(libraries: [
.init(
identifier: "ios-x86_64-simulator",
path: RelativePath("MyFramework.framework"),
architectures: [.x8664]
),
.init(
identifier: "ios-arm64",
path: RelativePath("MyFramework.framework"),
architectures: [.arm64]
),
])
let expectedBinaryPath = frameworkPath.appending(RelativePath("ios-x86_64-simulator/MyFramework.framework/MyFramework"))
XCTAssertEqual(metadata, XCFrameworkMetadata(
path: frameworkPath,
infoPlist: expectedInfoPlist,
primaryBinaryPath: expectedBinaryPath,
linking: .dynamic
))
}
func test_loadMetadata_staticLibrary() throws {
// Given
let frameworkPath = fixturePath(path: RelativePath("MyStaticLibrary.xcframework"))
// When
let metadata = try subject.loadMetadata(at: frameworkPath)
// Then
let expectedInfoPlist = XCFrameworkInfoPlist(libraries: [
.init(
identifier: "ios-x86_64-simulator",
path: RelativePath("libMyStaticLibrary.a"),
architectures: [.x8664]
),
.init(
identifier: "ios-arm64",
path: RelativePath("libMyStaticLibrary.a"),
architectures: [.arm64]
),
])
let expectedBinaryPath = frameworkPath.appending(RelativePath("ios-x86_64-simulator/libMyStaticLibrary.a"))
XCTAssertEqual(metadata, XCFrameworkMetadata(
path: frameworkPath,
infoPlist: expectedInfoPlist,
primaryBinaryPath: expectedBinaryPath,
linking: .static
))
}
}

View File

@ -0,0 +1,766 @@
import Foundation
import TSCBasic
import TuistSupport
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
@testable import TuistSupportTesting
final class ValueGraphLoaderTests: TuistUnitTestCase {
private var stubbedFrameworks = [AbsolutePath: PrecompiledMetadata]()
private var stubbedLibraries = [AbsolutePath: PrecompiledMetadata]()
private var stubbedXCFrameworks = [AbsolutePath: XCFrameworkMetadata]()
private var frameworkMetadataProvider: MockFrameworkMetadataProvider!
private var libraryMetadataProvider: MockLibraryMetadataProvider!
private var xcframeworkMetadataProvider: MockXCFrameworkMetadataProvider!
override func setUpWithError() throws {
frameworkMetadataProvider = makeFrameworkMetadataProvider()
libraryMetadataProvider = makeLibraryMetadataProvider()
xcframeworkMetadataProvider = makeXCFrameworkMetadataProvider()
}
// MARK: - Load Workspace
func test_loadWorkspace_unreferencedProjectsAreExcluded() throws {
// Given
let projectA = Project.test(path: "/A", name: "A", targets: [])
let projectB = Project.test(path: "/B", name: "B", targets: [])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(graph.workspace, workspace)
XCTAssertEqual(graph.projects, [
"/A": projectA,
])
XCTAssertTrue(graph.targets.isEmpty)
XCTAssertTrue(graph.dependencies.isEmpty)
}
func test_loadWorkspace_unlinkedReferencedProjectsAreIncluded() throws {
// Given
let projectA = Project.test(path: "/A", name: "A", targets: [])
let projectB = Project.test(path: "/B", name: "B", targets: [])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B"])
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(graph.workspace, workspace)
XCTAssertEqual(graph.projects, [
"/A": projectA,
"/B": projectB,
])
XCTAssertTrue(graph.targets.isEmpty)
XCTAssertTrue(graph.dependencies.isEmpty)
}
func test_loadWorkspace_linkedReferencedProjectsAreIncluded() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "B", path: "/B")])
let targetB = Target.test(name: "B", dependencies: [])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(graph.workspace, workspace.replacing(projects: ["/A", "/B"]))
XCTAssertEqual(graph.projects, [
"/A": projectA,
"/B": projectB,
])
XCTAssertEqual(graph.targets, [
"/A": ["A": targetA],
"/B": ["B": targetB],
])
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.target(name: "B", path: "/B"),
]),
])
}
// MARK: - Load Project
func test_loadProject_unlinkedProjectsAreExcluded() throws {
// Given
let projectA = Project.test(path: "/A", name: "A", targets: [])
let projectB = Project.test(path: "/B", name: "B", targets: [])
let subject = makeSubject()
// When
let (loadedProject, graph) = try subject.loadProject(at: "/A", projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(loadedProject, projectA)
XCTAssertEqual(graph.projects, [
"/A": projectA,
])
XCTAssertTrue(graph.targets.isEmpty)
XCTAssertTrue(graph.dependencies.isEmpty)
}
func test_loadProject_linkedProjectsAreIncluded() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "B", path: "/B")])
let targetB = Target.test(name: "B", dependencies: [])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let subject = makeSubject()
// When
let (loadedProject, graph) = try subject.loadProject(at: "/A", projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(loadedProject, projectA)
XCTAssertEqual(graph.projects, [
"/A": projectA,
"/B": projectB,
])
XCTAssertEqual(graph.targets, [
"/A": ["A": targetA],
"/B": ["B": targetB],
])
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.target(name: "B", path: "/B"),
]),
])
}
// MARK: - Frameworks
func test_loadWorkspace_frameworkDependency() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.framework(path: "/Frameworks/F1.framework")])
let targetB = Target.test(name: "B", dependencies: [.framework(path: "/Frameworks/F2.framework")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B"])
stubFramework(
metadata: .init(
path: "/Frameworks/F1.framework",
linkage: .dynamic,
architectures: [.arm64]
)
)
stubFramework(
metadata: .init(
path: "/Frameworks/F2.framework",
linkage: .static,
architectures: [.x8664]
)
)
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.framework(
path: "/Frameworks/F1.framework",
binaryPath: "/Frameworks/F1.framework/F1",
dsymPath: nil,
bcsymbolmapPaths: [],
linking: .dynamic,
architectures: [.arm64],
isCarthage: false
),
]),
.target(name: "B", path: "/B"): Set([
.framework(
path: "/Frameworks/F2.framework",
binaryPath: "/Frameworks/F2.framework/F2",
dsymPath: nil,
bcsymbolmapPaths: [],
linking: .static,
architectures: [.x8664],
isCarthage: false
),
]),
])
}
func test_loadWorkspace_frameworkDependencyReferencedMultipleTimes() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.framework(path: "/Frameworks/F.framework")])
let targetB = Target.test(name: "B", dependencies: [.framework(path: "/Frameworks/F.framework")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B"])
stubFramework(
metadata: .init(
path: "/Frameworks/F.framework",
linkage: .dynamic,
architectures: [.arm64]
)
)
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
let frameworkDependency: ValueGraphDependency = .framework(
path: "/Frameworks/F.framework",
binaryPath: "/Frameworks/F.framework/F",
dsymPath: nil,
bcsymbolmapPaths: [],
linking: .dynamic,
architectures: [.arm64],
isCarthage: false
)
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
frameworkDependency,
]),
.target(name: "B", path: "/B"): Set([
frameworkDependency,
]),
])
}
// MARK: - Libraries
func test_loadWorkspace_libraryDependency() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [
.library(path: "/libs/lib1/libL1.dylib", publicHeaders: "/libs/lib1/include", swiftModuleMap: nil),
])
let targetB = Target.test(name: "B", dependencies: [
.library(
path: "/libs/lib2/libL2.a",
publicHeaders: "/libs/lib2/include",
swiftModuleMap: "/libs/lib2.swiftmodule"
),
])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B"])
stubLibrary(
metadata: .init(
path: "/libs/lib1/libL1.dylib",
linkage: .dynamic,
architectures: [.arm64]
)
)
stubLibrary(
metadata: .init(
path: "/libs/lib2/libL2.a",
linkage: .static,
architectures: [.x8664]
)
)
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
])
// Then
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.library(
path: "/libs/lib1/libL1.dylib",
publicHeaders: "/libs/lib1/include",
linking: .dynamic,
architectures: [.arm64],
swiftModuleMap: nil
),
]),
.target(name: "B", path: "/B"): Set([
.library(
path: "/libs/lib2/libL2.a",
publicHeaders: "/libs/lib2/include",
linking: .static,
architectures: [.x8664],
swiftModuleMap: "/libs/lib2.swiftmodule"
),
]),
])
}
// MARK: - XCFrameworks
func test_loadWorkspace_xcframeworkDependency() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.xcFramework(path: "/XCFrameworks/XF1.xcframework")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
stubXCFramework(
metadata: .init(
path: "/XCFrameworks/XF1.xcframework",
infoPlist: .test(),
primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1",
linking: .dynamic
)
)
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
])
// Then
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.xcframework(
path: "/XCFrameworks/XF1.xcframework",
infoPlist: .test(),
primaryBinaryPath: "/XCFrameworks/XF1.xcframework/ios-arm64/XF1",
linking: .dynamic
),
]),
])
}
// MARK: - SDKs
func test_loadWorkspace_sdkDependency() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.sdk(name: "libc++.tbd", status: .required)])
let targetB = Target.test(name: "B", dependencies: [.sdk(name: "SwiftUI.framework", status: .optional)])
let targetC = Target.test(name: "C", dependencies: [.xctest])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA, targetB, targetC])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
])
// Then
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.sdk(
name: "libc++.tbd",
path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/libc++.tbd",
status: .required,
source: .system
),
]),
.target(name: "B", path: "/A"): Set([
.sdk(
name: "SwiftUI.framework",
path: "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework",
status: .optional,
source: .system
),
]),
.target(name: "C", path: "/A"): Set([
.sdk(
name: "XCTest.framework",
path: "/Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework",
status: .required,
source: .developer
),
]),
])
}
// MARK: - Packages
func test_loadWorkspace_packages() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [
.package(product: "PackageLibraryA1"),
])
let targetB = Target.test(name: "B", dependencies: [
.package(product: "PackageLibraryA2"),
])
let targetC = Target.test(name: "C", dependencies: [
.package(product: "PackageLibraryB"),
])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA], packages: [
.local(path: "/Packages/PackageLibraryA"),
])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB], packages: [
.local(path: "/Packages/PackageLibraryA"),
])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC], packages: [
.remote(url: "https://example.com/package-library-b", requirement: .branch("testing")),
])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B", "/C"])
let subject = makeSubject()
// When
let graph = try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
projectC,
])
// Then
// Note: the following is a reflection of the current implementation
// which has a few limitation / bugs when it comes to identifying the same
// package referenced by multiple projects/
XCTAssertEqual(graph.packages, [
"/A": ["/Packages/PackageLibraryA": .local(path: "/Packages/PackageLibraryA")],
"/B": ["/Packages/PackageLibraryA": .local(path: "/Packages/PackageLibraryA")],
"/C": ["https://example.com/package-library-b": .remote(url: "https://example.com/package-library-b", requirement: .branch("testing"))],
])
XCTAssertEqual(graph.dependencies, [
.target(name: "A", path: "/A"): Set([
.packageProduct(path: "/A", product: "PackageLibraryA1"),
]),
.target(name: "B", path: "/B"): Set([
.packageProduct(path: "/B", product: "PackageLibraryA2"),
]),
.target(name: "C", path: "/C"): Set([
.packageProduct(path: "/C", product: "PackageLibraryB"),
]),
])
}
// MARK: - Dependency Cycle
func test_loadProject_localDependencyCycle() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.target(name: "B")])
let targetB = Target.test(name: "B", dependencies: [.target(name: "C")])
let targetC = Target.test(name: "C", dependencies: [.target(name: "A")])
let project = Project.test(path: "/A", name: "A", targets: [targetA, targetB, targetC])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadProject(at: "/A", projects: [
project,
]),
GraphLoadingError.circularDependency([
.init(path: "/A", name: "A"),
.init(path: "/A", name: "B"),
.init(path: "/A", name: "C"),
])
)
}
func test_loadProject_differentProjectDependencyCycle() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "B", path: "/B")])
let targetB = Target.test(name: "B", dependencies: [.project(target: "C", path: "/C")])
let targetC = Target.test(name: "C", dependencies: [.project(target: "A", path: "/A")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadProject(at: "/A", projects: [
projectA,
projectB,
projectC,
]),
GraphLoadingError.circularDependency([
.init(path: "/A", name: "A"),
.init(path: "/B", name: "B"),
.init(path: "/C", name: "C"),
])
)
}
func test_loadWorkspace_differentProjectDependencyCycle() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "B", path: "/B")])
let targetB = Target.test(name: "B", dependencies: [.project(target: "C", path: "/C")])
let targetC = Target.test(name: "C", dependencies: [.project(target: "A", path: "/A")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B", "/C"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
projectC,
]),
GraphLoadingError.circularDependency([
.init(path: "/A", name: "A"),
.init(path: "/B", name: "B"),
.init(path: "/C", name: "C"),
])
)
}
func test_loadProject_crossProjectsReferenceWithNoDependencyCycle() throws {
// Given
let targetA1 = Target.test(name: "A1", dependencies: [.project(target: "B1", path: "/B")])
let targetA2 = Target.test(name: "A2", dependencies: [.project(target: "B2", path: "/B")])
let targetB1 = Target.test(name: "B1", dependencies: [.project(target: "C", path: "/C")])
let targetB2 = Target.test(name: "B2", dependencies: [])
let targetC = Target.test(name: "C", dependencies: [.project(target: "A2", path: "/A")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA1, targetA2])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB1, targetB2])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC])
let subject = makeSubject()
// When / Then
XCTAssertNoThrow(
try subject.loadProject(at: "/A", projects: [
projectA,
projectB,
projectC,
])
)
}
func test_loadWorkspace_crossProjectsReferenceWithNoDependencyCycle() throws {
// Given
let targetA1 = Target.test(name: "A1", dependencies: [.project(target: "B1", path: "/B")])
let targetA2 = Target.test(name: "A2", dependencies: [.project(target: "B2", path: "/B")])
let targetB1 = Target.test(name: "B1", dependencies: [.project(target: "C", path: "/C")])
let targetB2 = Target.test(name: "B2", dependencies: [])
let targetC = Target.test(name: "C", dependencies: [.project(target: "A2", path: "/A")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA1, targetA2])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB1, targetB2])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B", "/C"])
let subject = makeSubject()
// When / Then
XCTAssertNoThrow(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
projectC,
])
)
}
func test_loadWorkspace_crossProjectsReferenceWithDependencyCycle() throws {
// Given
let targetA1 = Target.test(name: "A1", dependencies: [.project(target: "B1", path: "/B")])
let targetA2 = Target.test(name: "A2", dependencies: [.project(target: "B2", path: "/B")])
let targetB1 = Target.test(name: "B1", dependencies: [.project(target: "C1", path: "/C")])
let targetB2 = Target.test(name: "B2", dependencies: [.project(target: "C2", path: "/C")])
let targetC1 = Target.test(name: "C1", dependencies: [.project(target: "A2", path: "/A")])
let targetC2 = Target.test(name: "C2", dependencies: [.project(target: "B1", path: "/B")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA1, targetA2])
let projectB = Project.test(path: "/B", name: "B", targets: [targetB1, targetB2])
let projectC = Project.test(path: "/C", name: "C", targets: [targetC1, targetC2])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B", "/C"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsError(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
projectC,
])
) { error in
// need to manually inspect the error as depending on traversal order may result in different nodes getting listed
let graphError = error as? GraphLoadingError
XCTAssertNotNil(graphError)
XCTAssertTrue(graphError?.isCycleError == true)
}
}
// MARK: - Error Cases
func test_loadWorkspace_missingProjectReferenceInWorkspace() throws {
// Given
let projectA = Project.test(path: "/A", name: "A", targets: [])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/Missing"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
]),
GraphLoadingError.missingProject("/Missing")
)
}
func test_loadWorkspace_missingProjectReferenceInDependency() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "Missing", path: "/Missing")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
]),
GraphLoadingError.missingProject("/Missing")
)
}
func test_loadWorkspace_missingTargetReferenceInLocalProject() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.target(name: "Missing")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
]),
GraphLoadingError.targetNotFound("Missing", "/A")
)
}
func test_loadWorkspace_missingTargetReferenceInOtherProject() throws {
// Given
let targetA = Target.test(name: "A", dependencies: [.project(target: "Missing", path: "/B")])
let projectA = Project.test(path: "/A", name: "A", targets: [targetA])
let projectB = Project.test(path: "/B", name: "B", targets: [])
let workspace = Workspace.test(path: "/", name: "Workspace", projects: ["/A", "/B"])
let subject = makeSubject()
// When / Then
XCTAssertThrowsSpecific(
try subject.loadWorkspace(workspace: workspace, projects: [
projectA,
projectB,
]),
GraphLoadingError.targetNotFound("Missing", "/B")
)
}
// MARK: - Helpers
private func makeSubject() -> ValueGraphLoader {
ValueGraphLoader(
frameworkMetadataProvider: frameworkMetadataProvider,
libraryMetadataProvider: libraryMetadataProvider,
xcframeworkMetadataProvider: xcframeworkMetadataProvider,
systemFrameworkMetadataProvider: SystemFrameworkMetadataProvider()
)
}
private func makeFrameworkMetadataProvider() -> MockFrameworkMetadataProvider {
let provider = MockFrameworkMetadataProvider()
provider.loadMetadataStub = { [weak self] path in
guard let metadata = self?.stubbedFrameworks[path] else {
throw FrameworkMetadataProviderError.frameworkNotFound(path)
}
return FrameworkMetadata(
path: path,
binaryPath: path.appending(component: path.basenameWithoutExt),
dsymPath: nil,
bcsymbolmapPaths: [],
linking: metadata.linkage,
architectures: metadata.architectures,
isCarthage: false
)
}
return provider
}
private func makeLibraryMetadataProvider() -> MockLibraryMetadataProvider {
let provider = MockLibraryMetadataProvider()
provider.loadMetadataStub = { [weak self] path, publicHeaders, swiftModuleMap in
guard let metadata = self?.stubbedLibraries[path] else {
throw LibraryMetadataProviderError.libraryNotFound(path)
}
return LibraryMetadata(
path: path,
publicHeaders: publicHeaders,
swiftModuleMap: swiftModuleMap,
architectures: metadata.architectures,
linking: metadata.linkage
)
}
return provider
}
private func makeXCFrameworkMetadataProvider() -> MockXCFrameworkMetadataProvider {
let provider = MockXCFrameworkMetadataProvider()
provider.loadMetadataStub = { [weak self] path in
guard let metadata = self?.stubbedXCFrameworks[path] else {
throw XCFrameworkMetadataProviderError.xcframeworkNotFound(path)
}
return metadata
}
return provider
}
private func stubFramework(metadata: PrecompiledMetadata) {
stubbedFrameworks[metadata.path] = metadata
}
private func stubLibrary(metadata: PrecompiledMetadata) {
stubbedLibraries[metadata.path] = metadata
}
private func stubXCFramework(metadata: XCFrameworkMetadata) {
stubbedXCFrameworks[metadata.path] = metadata
}
// MARK: - Helper types
private struct PrecompiledMetadata {
var path: AbsolutePath
var linkage: BinaryLinking
var architectures: [BinaryArchitecture]
}
}
private extension GraphLoadingError {
var isCycleError: Bool {
switch self {
case .circularDependency:
return true
default:
return false
}
}
}