Prevent generation of redundant file elements (#515)
- File elements for all dependencies are products within a graph were always generated and included in projects without applying any filtering - This led to the inclusion of redundant file elements in top level projects (for products & dependencies that they didn't require) - The graph helper methods are now consulted to obtain a list of dependency references that are known to be required by the project - Additional helpers methods on graph were add to help unify / share some of the business logic regarding which dependencies are needed Test Plan: - Generate the fixture `fixtures/ios_app_with_frameworks` via `tuist generate` - Open the generated Workspace - Verify the `Framework3` project doesn't contain file references for `Framework5` - Verify the `Framework3` project doesn't contain file references for `ARKit.framework` - Verify the `Framework4` project doesn't contain file references for `ARKit.framework`
This commit is contained in:
parent
11da16b36f
commit
b6c822d520
|
@ -28,6 +28,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
- Product name linting failing when it contains variables https://github.com/tuist/tuist/pull/494 by @dcvz
|
||||
- Build phases not generated in the right position https://github.com/tuist/tuist/pull/506 by @pepibumur
|
||||
- Remove \$(SRCROOT) from being included in `Info.plist` path https://github.com/tuist/tuist/pull/511 by @dcvz
|
||||
- Prevent generation of redundant file elements https://github.com/tuist/tuist/pull/515 by @kwridan
|
||||
|
||||
## 0.17.0
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ final class LinkGenerator: LinkGenerating {
|
|||
precompiledEmbedPhase.inputPaths.append(relativePath)
|
||||
precompiledEmbedPhase.outputPaths.append("$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/\(path.components.last!)")
|
||||
|
||||
} else if case let DependencyReference.product(target) = dependency {
|
||||
} else if case let DependencyReference.product(target, _) = dependency {
|
||||
guard let fileRef = fileElements.product(target: target) else {
|
||||
throw LinkGeneratorError.missingProduct(name: target)
|
||||
}
|
||||
|
@ -283,7 +283,7 @@ final class LinkGenerator: LinkGenerating {
|
|||
let buildFile = PBXBuildFile(file: fileRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
buildPhase.files?.append(buildFile)
|
||||
case let .product(target):
|
||||
case let .product(target, _):
|
||||
guard let fileRef = fileElements.product(target: target) else {
|
||||
throw LinkGeneratorError.missingProduct(name: target)
|
||||
}
|
||||
|
@ -322,14 +322,7 @@ final class LinkGenerator: LinkGenerating {
|
|||
// This technique also allows resource bundles that reside in different projects to get built ahead of the
|
||||
// "Copy Bundle Resources" phase.
|
||||
|
||||
var dependencies = [DependencyReference]()
|
||||
if target.product.isStatic {
|
||||
dependencies.append(contentsOf: graph.staticDependencies(path: path, name: target.name))
|
||||
}
|
||||
|
||||
dependencies.append(contentsOf:
|
||||
graph.resourceBundleDependencies(path: path, name: target.name)
|
||||
.map { .product(target: $0.target.name) })
|
||||
let dependencies = graph.copyProductDependencies(path: path, target: target)
|
||||
|
||||
if !dependencies.isEmpty {
|
||||
try generateDependenciesBuildPhase(
|
||||
|
@ -347,7 +340,7 @@ final class LinkGenerator: LinkGenerating {
|
|||
fileElements: ProjectFileElements) throws {
|
||||
var files: [PBXBuildFile] = []
|
||||
|
||||
for case let .product(target) in dependencies.sorted() {
|
||||
for case let .product(target, _) in dependencies.sorted() {
|
||||
guard let fileRef = fileElements.product(target: target) else {
|
||||
throw LinkGeneratorError.missingProduct(name: target)
|
||||
}
|
||||
|
|
|
@ -33,15 +33,18 @@ class ProjectFileElements {
|
|||
var sdks: [AbsolutePath: PBXFileReference] = [:]
|
||||
let playgrounds: Playgrounding
|
||||
let filesSortener: ProjectFilesSortening
|
||||
private let system: Systeming
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(_ elements: [AbsolutePath: PBXFileElement] = [:],
|
||||
playgrounds: Playgrounding = Playgrounds(),
|
||||
filesSortener: ProjectFilesSortening = ProjectFilesSortener()) {
|
||||
filesSortener: ProjectFilesSortening = ProjectFilesSortener(),
|
||||
system: Systeming = System()) {
|
||||
self.elements = elements
|
||||
self.playgrounds = playgrounds
|
||||
self.filesSortener = filesSortener
|
||||
self.system = system
|
||||
}
|
||||
|
||||
func generateProjectFiles(project: Project,
|
||||
|
@ -50,10 +53,9 @@ class ProjectFileElements {
|
|||
pbxproj: PBXProj,
|
||||
sourceRootPath: AbsolutePath) throws {
|
||||
var files = Set<GroupFileElement>()
|
||||
var products = Set<String>()
|
||||
project.targets.forEach { target in
|
||||
files.formUnion(targetFiles(target: target, sourceRootPath: sourceRootPath))
|
||||
products.formUnion(targetProducts(target: target))
|
||||
|
||||
try project.targets.forEach { target in
|
||||
try files.formUnion(targetFiles(target: target, projectPath: project.path, graph: graph))
|
||||
}
|
||||
let projectFileElements = projectFiles(project: project)
|
||||
files.formUnion(projectFileElements)
|
||||
|
@ -75,17 +77,15 @@ class ProjectFileElements {
|
|||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
|
||||
let dependencies = graph.findAll(path: project.path)
|
||||
// Products
|
||||
let directProducts = project.targets.map {
|
||||
DependencyReference.product(target: $0.name, productName: $0.productNameWithExtension)
|
||||
}
|
||||
|
||||
/// Products
|
||||
try generateProducts(project: project,
|
||||
dependencies: dependencies,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
// Dependencies
|
||||
let dependencies = try graph.allDependencyReferences(for: project, system: system)
|
||||
|
||||
/// Dependencies
|
||||
try generate(dependencies: dependencies,
|
||||
path: project.path,
|
||||
try generate(dependencyReferences: Set(directProducts + dependencies),
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
|
@ -112,13 +112,7 @@ class ProjectFileElements {
|
|||
return fileElements
|
||||
}
|
||||
|
||||
func targetProducts(target: Target) -> Set<String> {
|
||||
var products: Set<String> = Set()
|
||||
products.insert(target.productNameWithExtension)
|
||||
return products
|
||||
}
|
||||
|
||||
func targetFiles(target: Target, sourceRootPath _: AbsolutePath) -> Set<GroupFileElement> {
|
||||
func targetFiles(target: Target, projectPath: AbsolutePath, graph: Graphing) throws -> Set<GroupFileElement> {
|
||||
var files = Set<AbsolutePath>()
|
||||
files.formUnion(target.sources.map { $0.path })
|
||||
files.formUnion(target.coreDataModels.map { $0.path })
|
||||
|
@ -153,6 +147,20 @@ class ProjectFileElements {
|
|||
isReference: $0.isReference)
|
||||
})
|
||||
|
||||
// Local Packages
|
||||
elements.formUnion(
|
||||
try graph.packages(path: projectPath, name: target.name).compactMap { node -> GroupFileElement? in
|
||||
switch node.packageType {
|
||||
case let .local(path: packagePath, productName: _):
|
||||
return GroupFileElement(path: projectPath.appending(packagePath),
|
||||
group: target.filesGroup,
|
||||
isReference: true)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
|
@ -184,86 +192,50 @@ class ProjectFileElements {
|
|||
}
|
||||
}
|
||||
|
||||
func generateProducts(project: Project,
|
||||
dependencies: Set<GraphNode>,
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj) throws {
|
||||
try prepareProductsFileReferences(project: project, dependencies: dependencies).forEach { pair in
|
||||
guard self.products[pair.targetName] == nil else { return }
|
||||
pbxproj.add(object: pair.fileReference)
|
||||
groups.products.children.append(pair.fileReference)
|
||||
self.products[pair.targetName] = pair.fileReference
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProductsFileReferences(project: Project, dependencies: Set<GraphNode>)
|
||||
throws -> [(targetName: String, fileReference: PBXFileReference)] {
|
||||
let targetsProducts = project.targets
|
||||
.map { ($0, $0.product) }
|
||||
let dependenciesProducts = dependencies
|
||||
.compactMap { $0 as? TargetNode }
|
||||
.map { $0.target }
|
||||
.map { ($0, $0.product) }
|
||||
let mergeStrategy: (Product, Product) -> Product = { first, _ in first }
|
||||
let sortByName: ((Target, Product), (Target, Product)) -> Bool = { first, second in
|
||||
first.0.productNameWithExtension < second.0.productNameWithExtension
|
||||
}
|
||||
|
||||
let targetsProductsDictionary = Dictionary(targetsProducts, uniquingKeysWith: mergeStrategy)
|
||||
let dependenciesProductsDictionary = Dictionary(dependenciesProducts, uniquingKeysWith: mergeStrategy)
|
||||
let productsDictionary = targetsProductsDictionary.merging(dependenciesProductsDictionary,
|
||||
uniquingKeysWith: mergeStrategy)
|
||||
return productsDictionary
|
||||
.sorted(by: sortByName)
|
||||
.map { target, product in
|
||||
let fileType = Xcode.filetype(extension: product.xcodeValue.fileExtension!)
|
||||
return (targetName: target.name,
|
||||
fileReference: PBXFileReference(sourceTree: .buildProductsDir,
|
||||
explicitFileType: fileType,
|
||||
path: target.productNameWithExtension,
|
||||
includeInIndex: false))
|
||||
}
|
||||
}
|
||||
|
||||
func generate(dependencies: Set<GraphNode>,
|
||||
path _: AbsolutePath,
|
||||
func generate(dependencyReferences: Set<DependencyReference>,
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj,
|
||||
sourceRootPath: AbsolutePath,
|
||||
filesGroup: ProjectGroup) throws {
|
||||
let sortedDependencies = dependencies.sorted(by: { $0.path < $1.path })
|
||||
try sortedDependencies.forEach { node in
|
||||
switch node {
|
||||
case let precompiledNode as PrecompiledNode:
|
||||
let fileElement = GroupFileElement(path: precompiledNode.path,
|
||||
let sortedDependencies = dependencyReferences.sorted()
|
||||
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 sdkNode as SDKNode:
|
||||
generateSDKFileElement(node: sdkNode,
|
||||
case let .sdk(sdkNodePath, _):
|
||||
generateSDKFileElement(sdkNodePath: sdkNodePath,
|
||||
toGroup: groups.frameworks,
|
||||
pbxproj: pbxproj)
|
||||
case let packageNode as PackageNode:
|
||||
switch packageNode.packageType {
|
||||
case let .local(path: packagePath, productName: _):
|
||||
let fileElement = GroupFileElement(path: sourceRootPath.appending(packagePath),
|
||||
group: filesGroup)
|
||||
try generate(fileElement: fileElement,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
case .remote:
|
||||
// Only local packages need group, remote are handled by Xcode itself
|
||||
break
|
||||
}
|
||||
default:
|
||||
return
|
||||
case let .product(target: target, productName: productName):
|
||||
generateProduct(targetName: target,
|
||||
productName: productName,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateProduct(targetName: String,
|
||||
productName: String,
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj) {
|
||||
guard products[targetName] == nil else { return }
|
||||
let fileType = RelativePath(productName).extension.flatMap { Xcode.filetype(extension: $0) }
|
||||
let fileReference = PBXFileReference(sourceTree: .buildProductsDir,
|
||||
explicitFileType: fileType,
|
||||
path: productName,
|
||||
includeInIndex: false)
|
||||
|
||||
pbxproj.add(object: fileReference)
|
||||
groups.products.children.append(fileReference)
|
||||
products[targetName] = fileReference
|
||||
}
|
||||
|
||||
func generate(fileElement: GroupFileElement,
|
||||
groups: ProjectGroups,
|
||||
pbxproj: PBXProj,
|
||||
|
@ -469,20 +441,20 @@ class ProjectFileElements {
|
|||
elements[fileAbsolutePath] = file
|
||||
}
|
||||
|
||||
private func generateSDKFileElement(node: SDKNode,
|
||||
private func generateSDKFileElement(sdkNodePath: AbsolutePath,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) {
|
||||
guard sdks[node.path] == nil else {
|
||||
guard sdks[sdkNodePath] == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
addSDKElement(node: node, toGroup: toGroup, pbxproj: pbxproj)
|
||||
addSDKElement(sdkNodePath: sdkNodePath, toGroup: toGroup, pbxproj: pbxproj)
|
||||
}
|
||||
|
||||
private func addSDKElement(node: SDKNode,
|
||||
private func addSDKElement(sdkNodePath: AbsolutePath,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) {
|
||||
let sdkPath = node.path.relative(to: AbsolutePath("/")) // SDK paths are relative
|
||||
let sdkPath = sdkNodePath.relative(to: AbsolutePath("/")) // SDK paths are relative
|
||||
|
||||
let lastKnownFileType = sdkPath.extension.flatMap { Xcode.filetype(extension: $0) }
|
||||
let file = PBXFileReference(sourceTree: .developerDir,
|
||||
|
@ -491,7 +463,7 @@ class ProjectFileElements {
|
|||
path: sdkPath.pathString)
|
||||
pbxproj.add(object: file)
|
||||
toGroup.children.append(file)
|
||||
sdks[node.path] = file
|
||||
sdks[sdkNodePath] = file
|
||||
}
|
||||
|
||||
func group(path: AbsolutePath) -> PBXGroup? {
|
||||
|
|
|
@ -22,15 +22,16 @@ enum GraphError: FatalError {
|
|||
|
||||
enum DependencyReference: Equatable, Comparable, Hashable {
|
||||
case absolute(AbsolutePath)
|
||||
case product(target: String)
|
||||
case product(target: String, productName: String)
|
||||
case sdk(AbsolutePath, SDKStatus)
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .absolute(path):
|
||||
hasher.combine(path)
|
||||
case let .product(target):
|
||||
case let .product(target, productName):
|
||||
hasher.combine(target)
|
||||
hasher.combine(productName)
|
||||
case let .sdk(path, status):
|
||||
hasher.combine(path)
|
||||
hasher.combine(status)
|
||||
|
@ -41,8 +42,8 @@ enum DependencyReference: Equatable, Comparable, Hashable {
|
|||
switch (lhs, rhs) {
|
||||
case let (.absolute(lhsPath), .absolute(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
case let (.product(lhsName), .product(rhsName)):
|
||||
return lhsName == rhsName
|
||||
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
|
||||
default:
|
||||
|
@ -54,8 +55,11 @@ enum DependencyReference: Equatable, Comparable, Hashable {
|
|||
switch (lhs, rhs) {
|
||||
case let (.absolute(lhsPath), .absolute(rhsPath)):
|
||||
return lhsPath < rhsPath
|
||||
case let (.product(lhsName), .product(rhsName)):
|
||||
return lhsName < rhsName
|
||||
case let (.product(lhsTarget, lhsProductName), .product(rhsTarget, rhsProductName)):
|
||||
if lhsTarget == rhsTarget {
|
||||
return lhsProductName < rhsProductName
|
||||
}
|
||||
return lhsTarget < rhsTarget
|
||||
case let (.sdk(lhsPath, _), .sdk(rhsPath, _)):
|
||||
return lhsPath < rhsPath
|
||||
case (.sdk, .absolute):
|
||||
|
@ -101,6 +105,12 @@ protocol Graphing: AnyObject, Encodable {
|
|||
func staticDependencies(path: AbsolutePath, name: String) -> [DependencyReference]
|
||||
func resourceBundleDependencies(path: AbsolutePath, name: String) -> [TargetNode]
|
||||
|
||||
/// Products that are added to a dummy copy files phase to enforce build order between dependencies that Xcode doesn't usually respect (e.g. Resouce Bundles)
|
||||
func copyProductDependencies(path: AbsolutePath, target: Target) -> [DependencyReference]
|
||||
|
||||
/// All dependency referrences expected to present within a Project
|
||||
func allDependencyReferences(for project: Project, system: Systeming) throws -> [DependencyReference]
|
||||
|
||||
// MARK: - Depth First Search
|
||||
|
||||
/// Depth-first search (DFS) is an algorithm for traversing graph data structures. It starts at a source node
|
||||
|
@ -182,7 +192,7 @@ class Graph: Graphing {
|
|||
|
||||
return targetNode.targetDependencies
|
||||
.filter(isStaticLibrary)
|
||||
.map { DependencyReference.product(target: $0.target.name) }
|
||||
.map { DependencyReference.product(target: $0.target.name, productName: $0.target.productNameWithExtension) }
|
||||
}
|
||||
|
||||
func resourceBundleDependencies(path: AbsolutePath, name: String) -> [TargetNode] {
|
||||
|
@ -241,13 +251,13 @@ class Graph: Graphing {
|
|||
if targetNode.target.canLinkStaticProducts() {
|
||||
let staticLibraryTargetNodes = findAll(targetNode: targetNode, test: isStaticLibrary, skip: isFramework)
|
||||
let staticLibraries = staticLibraryTargetNodes.map {
|
||||
DependencyReference.product(target: $0.target.name)
|
||||
DependencyReference.product(target: $0.target.name, productName: $0.target.productNameWithExtension)
|
||||
}
|
||||
|
||||
let staticDependenciesDynamicLibraries = staticLibraryTargetNodes.flatMap {
|
||||
$0.targetDependencies
|
||||
.filter(or(isFramework, isDynamicLibrary))
|
||||
.map { DependencyReference.product(target: $0.target.name) }
|
||||
.map { DependencyReference.product(target: $0.target.name, productName: $0.target.productNameWithExtension) }
|
||||
}
|
||||
|
||||
references = references.union(staticLibraries + staticDependenciesDynamicLibraries)
|
||||
|
@ -257,7 +267,7 @@ class Graph: Graphing {
|
|||
|
||||
let dynamicLibrariesAndFrameworks = targetNode.targetDependencies
|
||||
.filter(or(isFramework, isDynamicLibrary))
|
||||
.map { DependencyReference.product(target: $0.target.name) }
|
||||
.map { DependencyReference.product(target: $0.target.name, productName: $0.target.productNameWithExtension) }
|
||||
|
||||
references = references.union(dynamicLibrariesAndFrameworks)
|
||||
return Array(references).sorted()
|
||||
|
@ -321,13 +331,44 @@ class Graph: Graphing {
|
|||
|
||||
/// Other targets' frameworks.
|
||||
let otherTargetFrameworks = findAll(targetNode: targetNode, test: isFramework)
|
||||
.map { DependencyReference.product(target: $0.target.name) }
|
||||
.map { DependencyReference.product(target: $0.target.name, productName: $0.target.productNameWithExtension) }
|
||||
|
||||
references.append(contentsOf: otherTargetFrameworks)
|
||||
|
||||
return Set(references).sorted()
|
||||
}
|
||||
|
||||
func copyProductDependencies(path: AbsolutePath, target: Target) -> [DependencyReference] {
|
||||
var dependencies = [DependencyReference]()
|
||||
|
||||
if target.product.isStatic {
|
||||
dependencies.append(contentsOf: staticDependencies(path: path, name: target.name))
|
||||
}
|
||||
|
||||
dependencies.append(contentsOf:
|
||||
resourceBundleDependencies(path: path, name: target.name)
|
||||
.map { .product(target: $0.target.name, productName: $0.target.productNameWithExtension) })
|
||||
|
||||
return Set(dependencies).sorted()
|
||||
}
|
||||
|
||||
func allDependencyReferences(for project: Project, system: Systeming) throws -> [DependencyReference] {
|
||||
let linkableDependencies = try project.targets.flatMap {
|
||||
try self.linkableDependencies(path: project.path, name: $0.name, system: system)
|
||||
}
|
||||
|
||||
let embeddableDependencies = try project.targets.flatMap {
|
||||
try self.embeddableFrameworks(path: project.path, name: $0.name, system: system)
|
||||
}
|
||||
|
||||
let copyProductDependencies = project.targets.flatMap {
|
||||
self.copyProductDependencies(path: project.path, target: $0)
|
||||
}
|
||||
|
||||
let allDepdendencies = linkableDependencies + embeddableDependencies + copyProductDependencies
|
||||
return Set(allDepdendencies).sorted()
|
||||
}
|
||||
|
||||
// MARK: - Fileprivate
|
||||
|
||||
private func findTargetNode(path: AbsolutePath, name: String) -> TargetNode? {
|
||||
|
|
|
@ -27,7 +27,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
func test_generateEmbedPhase() throws {
|
||||
var dependencies: [DependencyReference] = []
|
||||
dependencies.append(DependencyReference.absolute(AbsolutePath("/test.framework")))
|
||||
dependencies.append(DependencyReference.product(target: "Test"))
|
||||
dependencies.append(DependencyReference.product(target: "Test", productName: "Test.framework"))
|
||||
let pbxproj = PBXProj()
|
||||
let pbxTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = ProjectFileElements()
|
||||
|
@ -58,7 +58,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
|
||||
func test_generateEmbedPhase_throws_when_aProductIsMissing() throws {
|
||||
var dependencies: [DependencyReference] = []
|
||||
dependencies.append(DependencyReference.product(target: "Test"))
|
||||
dependencies.append(DependencyReference.product(target: "Test", productName: "Test.framework"))
|
||||
let pbxproj = PBXProj()
|
||||
let pbxTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = ProjectFileElements()
|
||||
|
@ -218,7 +218,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
func test_generateLinkingPhase() throws {
|
||||
var dependencies: [DependencyReference] = []
|
||||
dependencies.append(DependencyReference.absolute(AbsolutePath("/test.framework")))
|
||||
dependencies.append(DependencyReference.product(target: "Test"))
|
||||
dependencies.append(DependencyReference.product(target: "Test", productName: "Test.framework"))
|
||||
let pbxproj = PBXProj()
|
||||
let pbxTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = ProjectFileElements()
|
||||
|
@ -260,7 +260,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
|
||||
func test_generateLinkingPhase_throws_whenProductIsMissing() throws {
|
||||
var dependencies: [DependencyReference] = []
|
||||
dependencies.append(DependencyReference.product(target: "Test"))
|
||||
dependencies.append(DependencyReference.product(target: "Test", productName: "Test.framework"))
|
||||
let pbxproj = PBXProj()
|
||||
let pbxTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = ProjectFileElements()
|
||||
|
|
|
@ -215,13 +215,8 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
])
|
||||
}
|
||||
|
||||
func test_targetProducts() {
|
||||
let target = Target.test()
|
||||
let products = subject.targetProducts(target: target).sorted()
|
||||
XCTAssertEqual(products.first, "Target.app")
|
||||
}
|
||||
|
||||
func test_targetFiles() {
|
||||
func test_targetFiles() throws {
|
||||
// Given
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
|
||||
let settings = Settings.test(
|
||||
|
@ -250,7 +245,10 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
project: [AbsolutePath("/project/project.h")]),
|
||||
dependencies: [])
|
||||
|
||||
let files = subject.targetFiles(target: target, sourceRootPath: sourceRootPath)
|
||||
// When
|
||||
let files = try subject.targetFiles(target: target, projectPath: sourceRootPath, graph: Graph.test())
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(files.isSuperset(of: [
|
||||
GroupFileElement(path: "/project/debug.xcconfig", group: target.filesGroup),
|
||||
GroupFileElement(path: "/project/release.xcconfig", group: target.filesGroup),
|
||||
|
@ -266,130 +264,92 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
}
|
||||
|
||||
func test_generateProduct() throws {
|
||||
let pbxproj = PBXProj()
|
||||
let project = Project.test()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
try subject.generateProducts(project: project,
|
||||
dependencies: [],
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
XCTAssertEqual(groups.products.children.count, 1)
|
||||
let fileReference = subject.product(target: "Target")
|
||||
XCTAssertNotNil(fileReference)
|
||||
XCTAssertEqual(fileReference?.sourceTree, .buildProductsDir)
|
||||
XCTAssertEqual(fileReference?.path, "Target.app")
|
||||
XCTAssertNil(fileReference?.name)
|
||||
XCTAssertEqual(fileReference?.includeInIndex, false)
|
||||
}
|
||||
|
||||
func test_generateProducts_secondTime() throws {
|
||||
// Given
|
||||
let project = Project.test()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let pbxproj = PBXProj()
|
||||
let project = Project.test(targets: [
|
||||
.test(name: "App", product: .app),
|
||||
.test(name: "Framework", product: .framework),
|
||||
.test(name: "Library", product: .staticLibrary),
|
||||
])
|
||||
let graph = Graph.test()
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
try subject.generateProducts(project: project,
|
||||
dependencies: [],
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
let products = groups.products.children
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// When
|
||||
try subject.generateProducts(project: project,
|
||||
dependencies: [],
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
try subject.generateProjectFiles(project: project,
|
||||
graph: graph,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(groups.products.children, products) // we don't get duplicates
|
||||
XCTAssertEqual(groups.products.flattenedChildren, [
|
||||
"App.app",
|
||||
"Framework.framework",
|
||||
"libLibrary.a",
|
||||
])
|
||||
}
|
||||
|
||||
func test_generateProducts_stableOrder() throws {
|
||||
for _ in 0 ..< 5 {
|
||||
// Given
|
||||
let subject = ProjectFileElements(playgrounds: playgrounds)
|
||||
let pbxproj = PBXProj()
|
||||
let project = Project.test()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let subject = ProjectFileElements()
|
||||
let targets: [Target] = [
|
||||
.test(name: "App1", product: .app),
|
||||
.test(name: "App2", product: .app),
|
||||
.test(name: "Framework1", product: .framework),
|
||||
.test(name: "Framework2", product: .framework),
|
||||
.test(name: "Library1", product: .staticLibrary),
|
||||
.test(name: "Library2", product: .staticLibrary),
|
||||
].shuffled()
|
||||
|
||||
let project = Project.test(targets: targets)
|
||||
let graph = Graph.test()
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
let dependencies: Set<TargetNode> = Set((1 ... 5).map {
|
||||
let target = Target.test(name: "Target\($0)", product: .framework)
|
||||
return TargetNode(project: project,
|
||||
target: target,
|
||||
dependencies: [])
|
||||
})
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// When
|
||||
try subject.generateProducts(project: project,
|
||||
dependencies: dependencies,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj)
|
||||
try subject.generateProjectFiles(project: project,
|
||||
graph: graph,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// Then
|
||||
let expected = ["Target.app",
|
||||
"Target1.framework",
|
||||
"Target2.framework",
|
||||
"Target3.framework",
|
||||
"Target4.framework",
|
||||
"Target5.framework"]
|
||||
XCTAssertEqual(groups.products.children.map { $0.path }, expected)
|
||||
XCTAssertEqual(groups.products.flattenedChildren, [
|
||||
"App1.app",
|
||||
"App2.app",
|
||||
"Framework1.framework",
|
||||
"Framework2.framework",
|
||||
"libLibrary1.a",
|
||||
"libLibrary2.a",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func test_generateDependencies_whenTargetNode_thatHasAlreadyBeenAdded() throws {
|
||||
func test_generateProduct_fileReferencesProperties() throws {
|
||||
// Given
|
||||
let pbxproj = PBXProj()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let path = AbsolutePath("/test")
|
||||
let target = Target.test()
|
||||
let project = Project.test(path: path, targets: [target])
|
||||
let project = Project.test(targets: [
|
||||
.test(name: "App", product: .app),
|
||||
])
|
||||
let graph = Graph.test()
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
var dependencies: Set<GraphNode> = Set()
|
||||
let targetNode = TargetNode(project: project,
|
||||
target: target,
|
||||
dependencies: [])
|
||||
dependencies.insert(targetNode)
|
||||
sourceRootPath: project.path)
|
||||
|
||||
try subject.generate(dependencies: dependencies,
|
||||
path: path,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
// When
|
||||
try subject.generateProjectFiles(project: project,
|
||||
graph: graph,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: project.path)
|
||||
|
||||
XCTAssertEqual(groups.products.children.count, 0)
|
||||
}
|
||||
|
||||
func test_generateDependencies_whenTargetNode() throws {
|
||||
let pbxproj = PBXProj()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let path = AbsolutePath("/test")
|
||||
let target = Target.test()
|
||||
let project = Project.test(path: AbsolutePath("/waka"), targets: [target])
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
var dependencies: Set<GraphNode> = Set()
|
||||
let targetNode = TargetNode(project: project,
|
||||
target: target,
|
||||
dependencies: [])
|
||||
dependencies.insert(targetNode)
|
||||
|
||||
try subject.generate(dependencies: dependencies,
|
||||
path: path,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
|
||||
XCTAssertTrue(groups.products.children.isEmpty)
|
||||
// Then
|
||||
let fileReference = subject.product(target: "App")
|
||||
XCTAssertEqual(fileReference?.sourceTree, .buildProductsDir)
|
||||
}
|
||||
|
||||
func test_generateDependencies_whenPrecompiledNode() throws {
|
||||
|
@ -402,16 +362,15 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
var dependencies: Set<GraphNode> = Set()
|
||||
let precompiledNode = FrameworkNode(path: project.path.appending(component: "waka.framework"))
|
||||
var dependencies: Set<DependencyReference> = Set()
|
||||
let precompiledNode = DependencyReference.absolute(project.path.appending(component: "waka.framework"))
|
||||
dependencies.insert(precompiledNode)
|
||||
|
||||
try subject.generate(dependencies: dependencies,
|
||||
path: project.path,
|
||||
try subject.generate(dependencyReferences: dependencies,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
filesGroup: project.filesGroup)
|
||||
|
||||
let fileReference = groups.main.group(named: projectGroupName)?.children.first as? PBXFileReference
|
||||
XCTAssertEqual(fileReference?.path, "waka.framework")
|
||||
|
@ -605,10 +564,10 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
let sdk = try SDKNode(name: "ARKit.framework",
|
||||
platform: .iOS,
|
||||
status: .required)
|
||||
let sdkDependency = DependencyReference.sdk(sdk.path, sdk.status)
|
||||
|
||||
// When
|
||||
try subject.generate(dependencies: [sdk],
|
||||
path: sourceRootPath,
|
||||
try subject.generate(dependencyReferences: [sdkDependency],
|
||||
groups: groups, pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
|
@ -628,21 +587,24 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
func test_generateDependencies_localSwiftPackage() throws {
|
||||
// Given
|
||||
let pbxproj = PBXProj()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let target = Target.empty(name: "TargetA")
|
||||
let project = Project.empty(path: "/a/project",
|
||||
targets: [target])
|
||||
let groups = ProjectGroups.generate(project: .test(),
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
sourceRootPath: project.path)
|
||||
|
||||
let package = PackageNode(packageType: .local(path: RelativePath("packages/A"),
|
||||
productName: "A"),
|
||||
path: "/a/project/packages/A")
|
||||
let graph = createGraph(project: project, target: target, dependencies: [package])
|
||||
|
||||
// When
|
||||
try subject.generate(dependencies: [package],
|
||||
path: sourceRootPath,
|
||||
groups: groups, pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
try subject.generateProjectFiles(project: project,
|
||||
graph: graph,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
|
@ -654,27 +616,40 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
func test_generateDependencies_remoteSwiftPackage_doNotGenerateElements() throws {
|
||||
// Given
|
||||
let pbxproj = PBXProj()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let target = Target.empty(name: "TargetA")
|
||||
let project = Project.empty(path: "/a/project",
|
||||
targets: [target])
|
||||
let groups = ProjectGroups.generate(project: .test(),
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
sourceRootPath: project.path)
|
||||
|
||||
let package = PackageNode(packageType: .remote(url: "url",
|
||||
productName: "A",
|
||||
versionRequirement: .branch("master")),
|
||||
path: "/packages/url")
|
||||
|
||||
let graph = createGraph(project: project, target: target, dependencies: [package])
|
||||
|
||||
// When
|
||||
try subject.generate(dependencies: [package],
|
||||
path: sourceRootPath,
|
||||
groups: groups, pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
try subject.generateProjectFiles(project: project,
|
||||
graph: graph,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: project.path)
|
||||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func createGraph(project: Project, target: Target, dependencies: [GraphNode]) -> Graph {
|
||||
let targetNode = TargetNode(project: project, target: target, dependencies: dependencies)
|
||||
let cache = GraphLoaderCache()
|
||||
cache.add(targetNode: targetNode)
|
||||
return Graph.test(cache: cache)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PBXGroup {
|
||||
|
|
|
@ -88,7 +88,7 @@ final class GraphTests: XCTestCase {
|
|||
let got = try graph.linkableDependencies(path: project.path,
|
||||
name: target.name,
|
||||
system: system)
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency"))
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency", productName: "libDependency.a"))
|
||||
}
|
||||
|
||||
func test_linkableDependencies_whenAFrameworkTarget() throws {
|
||||
|
@ -117,14 +117,14 @@ final class GraphTests: XCTestCase {
|
|||
name: target.name,
|
||||
system: system)
|
||||
XCTAssertEqual(got.count, 1)
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency"))
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency", productName: "Dependency.framework"))
|
||||
|
||||
let frameworkGot = try graph.linkableDependencies(path: project.path,
|
||||
name: dependency.name,
|
||||
system: system)
|
||||
|
||||
XCTAssertEqual(frameworkGot.count, 1)
|
||||
XCTAssertTrue(frameworkGot.contains(.product(target: "StaticDependency")))
|
||||
XCTAssertTrue(frameworkGot.contains(.product(target: "StaticDependency", productName: "libStaticDependency.a")))
|
||||
}
|
||||
|
||||
func test_linkableDependencies_transitiveDynamicLibrariesOneStaticHop() throws {
|
||||
|
@ -151,8 +151,8 @@ final class GraphTests: XCTestCase {
|
|||
let result = try graph.linkableDependencies(path: projectA.path, name: app.name, system: system)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, [DependencyReference.product(target: "DynamicFramework"),
|
||||
DependencyReference.product(target: "StaticFramework")])
|
||||
XCTAssertEqual(result, [DependencyReference.product(target: "DynamicFramework", productName: "DynamicFramework.framework"),
|
||||
DependencyReference.product(target: "StaticFramework", productName: "StaticFramework.framework")])
|
||||
}
|
||||
|
||||
func test_linkableDependencies_transitiveDynamicLibrariesThreeHops() throws {
|
||||
|
@ -188,10 +188,14 @@ final class GraphTests: XCTestCase {
|
|||
let dynamicFramework1Result = try graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name, system: system)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(appResult, [DependencyReference.product(target: "DynamicFramework1")])
|
||||
XCTAssertEqual(dynamicFramework1Result, [DependencyReference.product(target: "DynamicFramework2"),
|
||||
DependencyReference.product(target: "StaticFramework1"),
|
||||
DependencyReference.product(target: "StaticFramework2")])
|
||||
XCTAssertEqual(appResult, [
|
||||
DependencyReference.product(target: "DynamicFramework1", productName: "DynamicFramework1.framework"),
|
||||
])
|
||||
XCTAssertEqual(dynamicFramework1Result, [
|
||||
DependencyReference.product(target: "DynamicFramework2", productName: "DynamicFramework2.framework"),
|
||||
DependencyReference.product(target: "StaticFramework1", productName: "libStaticFramework1.a"),
|
||||
DependencyReference.product(target: "StaticFramework2", productName: "libStaticFramework2.a"),
|
||||
])
|
||||
}
|
||||
|
||||
func test_linkableDependencies_transitiveDynamicLibrariesCheckNoDuplicatesInParentDynamic() throws {
|
||||
|
@ -230,7 +234,7 @@ final class GraphTests: XCTestCase {
|
|||
let dynamicFramework1Result = try graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name, system: system)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(dynamicFramework1Result, [DependencyReference.product(target: "DynamicFramework2")])
|
||||
XCTAssertEqual(dynamicFramework1Result, [DependencyReference.product(target: "DynamicFramework2", productName: "DynamicFramework2.framework")])
|
||||
}
|
||||
|
||||
func test_linkableDependencies_transitiveSDKDependenciesStatic() throws {
|
||||
|
@ -421,7 +425,7 @@ final class GraphTests: XCTestCase {
|
|||
let got = try graph.embeddableFrameworks(path: project.path,
|
||||
name: target.name,
|
||||
system: system)
|
||||
XCTAssertEqual(got.first, DependencyReference.product(target: "Dependency"))
|
||||
XCTAssertEqual(got.first, DependencyReference.product(target: "Dependency", productName: "Dependency.framework"))
|
||||
}
|
||||
|
||||
func test_embeddableFrameworks_when_dependencyIsAFramework() throws {
|
||||
|
@ -478,7 +482,7 @@ final class GraphTests: XCTestCase {
|
|||
)
|
||||
|
||||
XCTAssertEqual(got, [
|
||||
DependencyReference.product(target: "Dependency"),
|
||||
DependencyReference.product(target: "Dependency", productName: "Dependency.framework"),
|
||||
DependencyReference.absolute(frameworkPath),
|
||||
])
|
||||
}
|
||||
|
@ -535,7 +539,7 @@ final class GraphTests: XCTestCase {
|
|||
system: system)
|
||||
|
||||
// Then
|
||||
let expected = dependencyNames.sorted().map { DependencyReference.product(target: $0) }
|
||||
let expected = dependencyNames.sorted().map { DependencyReference.product(target: $0, productName: "\($0).framework") }
|
||||
XCTAssertEqual(got, expected)
|
||||
}
|
||||
|
||||
|
@ -719,18 +723,18 @@ final class DependencyReferenceTests: XCTestCase {
|
|||
let subjects: [(DependencyReference, DependencyReference, Bool)] = [
|
||||
// Absolute
|
||||
(.absolute(.init("/a.framework")), .absolute(.init("/a.framework")), true),
|
||||
(.absolute(.init("/a.framework")), .product(target: "Main"), false),
|
||||
(.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"), .product(target: "Main"), true),
|
||||
(.product(target: "Main"), .absolute(.init("/a.framework")), false),
|
||||
(.product(target: "Main"), .sdk(.init("/CoreData.framework"), .required), false),
|
||||
(.product(target: "Main-iOS"), .product(target: "Main-macOS"), false),
|
||||
(.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"), false),
|
||||
(.sdk(.init("/CoreData.framework"), .required), .product(target: "Main", productName: "Main.app"), false),
|
||||
(.sdk(.init("/CoreData.framework"), .required), .absolute(.init("/a.framework")), false),
|
||||
]
|
||||
|
||||
|
@ -742,17 +746,37 @@ final class DependencyReferenceTests: XCTestCase {
|
|||
XCTAssertTrue(DependencyReference.absolute("/A") < .absolute("/B"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/B") < .absolute("/A"))
|
||||
|
||||
XCTAssertFalse(DependencyReference.product(target: "A") < .product(target: "A"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "A") < .product(target: "B"))
|
||||
XCTAssertFalse(DependencyReference.product(target: "B") < .product(target: "A"))
|
||||
XCTAssertFalse(DependencyReference.product(target: "A", productName: "A.framework") < .product(target: "A", productName: "A.framework"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "A", productName: "A.framework") < .product(target: "B", productName: "B.framework"))
|
||||
XCTAssertFalse(DependencyReference.product(target: "B", productName: "B.framework") < .product(target: "A", productName: "A.framework"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "A", productName: "A.app") < .product(target: "A", productName: "A.framework"))
|
||||
|
||||
XCTAssertTrue(DependencyReference.product(target: "/A") < .absolute("/A"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "/A") < .absolute("/B"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "/B") < .absolute("/A"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "/A", productName: "A.framework") < .absolute("/A"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "/A", productName: "A.framework") < .absolute("/B"))
|
||||
XCTAssertTrue(DependencyReference.product(target: "/B", productName: "B.framework") < .absolute("/A"))
|
||||
|
||||
XCTAssertFalse(DependencyReference.absolute("/A") < .product(target: "/A"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/A") < .product(target: "/B"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/B") < .product(target: "/A"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/A") < .product(target: "/A", productName: "A.framework"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/A") < .product(target: "/B", productName: "B.framework"))
|
||||
XCTAssertFalse(DependencyReference.absolute("/B") < .product(target: "/A", productName: "A.framework"))
|
||||
}
|
||||
|
||||
func test_compare_isStable() {
|
||||
// Given
|
||||
let subject: [DependencyReference] = [
|
||||
.absolute("/A"),
|
||||
.absolute("/B"),
|
||||
.product(target: "A", productName: "A.framework"),
|
||||
.product(target: "B", productName: "B.framework"),
|
||||
.sdk("/A.framework", .required),
|
||||
.sdk("/B.framework", .optional),
|
||||
]
|
||||
|
||||
// When
|
||||
let sorted = (0 ..< 10).map { _ in subject.shuffled().sorted() }
|
||||
|
||||
// Then
|
||||
let unstable = sorted.dropFirst().filter { $0 != sorted.first }
|
||||
XCTAssertTrue(unstable.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,12 @@ Workspace:
|
|||
- Framework2:
|
||||
- Framework2 (dynamic iOS framework)
|
||||
- Framework2Tests (iOS unit tests)
|
||||
- Framework3:
|
||||
- Framework3 (dynamic iOS framework)
|
||||
- Framework4:
|
||||
- Framework4 (dynamic iOS framework)
|
||||
- Framework5:
|
||||
- Framework5 (dynamic iOS framework)
|
||||
```
|
||||
|
||||
Dependencies:
|
||||
|
@ -53,6 +59,9 @@ Dependencies:
|
|||
- App -> Framework1
|
||||
- App -> Framework2
|
||||
- Framework1 -> Framework2
|
||||
- Framework2 -> Framework3
|
||||
- Framework3 -> Framework4
|
||||
- Framework4 -> Framework5
|
||||
|
||||
## ios_app_with_framework_and_resources
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ let project = Project(name: "Framework2",
|
|||
headers: Headers(public: "Sources/Public/**",
|
||||
private: "Sources/Private/**",
|
||||
project: "Sources/Project/**"),
|
||||
dependencies: []),
|
||||
dependencies: [
|
||||
.project(target: "Framework3", path: "../Framework3")
|
||||
]),
|
||||
Target(name: "Framework2-macOS",
|
||||
platform: .macOS,
|
||||
product: .framework,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright Tuist©. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,16 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "Framework3",
|
||||
targets: [
|
||||
Target(name: "Framework3",
|
||||
platform: .iOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.Framework3",
|
||||
infoPlist: "Config/Framework3-Info.plist",
|
||||
sources: "Sources/**",
|
||||
dependencies: [
|
||||
.project(target: "Framework4", path: "../Framework4")
|
||||
]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
public class Framework3File {
|
||||
public init() {}
|
||||
|
||||
public func hello() -> String {
|
||||
return "Framework3File.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright Tuist©. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,16 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "Framework4",
|
||||
targets: [
|
||||
Target(name: "Framework4",
|
||||
platform: .iOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.Framework4",
|
||||
infoPlist: "Config/Framework4-Info.plist",
|
||||
sources: "Sources/**",
|
||||
dependencies: [
|
||||
.project(target: "Framework5", path: "../Framework5")
|
||||
]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
public class Framework4File {
|
||||
public init() {}
|
||||
|
||||
public func hello() -> String {
|
||||
return "Framework4File.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright Tuist©. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,16 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "Framework5",
|
||||
targets: [
|
||||
Target(name: "Framework5",
|
||||
platform: .iOS,
|
||||
product: .framework,
|
||||
bundleId: "io.tuist.Framework5",
|
||||
infoPlist: "Config/Framework5-Info.plist",
|
||||
sources: "Sources/**",
|
||||
dependencies: [
|
||||
.sdk(name: "ARKit.framework")
|
||||
]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
public class Framework5File {
|
||||
public init() {}
|
||||
|
||||
public func hello() -> String {
|
||||
return "Framework5File.hello()"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright Tuist©. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,16 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(
|
||||
name: "FrameworkA",
|
||||
targets: [
|
||||
Target(name: "FrameworkA",
|
||||
platform: .iOS,
|
||||
product: .staticFramework,
|
||||
bundleId: "io.tuist.FrameworkA",
|
||||
infoPlist: "Config/FrameworkA-Info.plist",
|
||||
sources: "Sources/**",
|
||||
dependencies: [
|
||||
.package(path: "../../Packages/PackageA", productName: "LibraryA"),
|
||||
]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
import Foundation
|
||||
import LibraryA
|
||||
|
||||
public class FrameworkAClass {
|
||||
public let text: String
|
||||
public init() {
|
||||
let libraryAClass = LibraryAClass()
|
||||
text = "FrameworkAClass::\(libraryAClass.text)"
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ let project = Project(name: "App",
|
|||
// "Resources/**"
|
||||
],
|
||||
dependencies: [
|
||||
.project(target: "FrameworkA", path: "Frameworks/FrameworkA"),
|
||||
.package(path: "Packages/PackageA", productName: "LibraryA"),
|
||||
.package(path: "Packages/PackageA", productName: "LibraryB"),
|
||||
]),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import UIKit
|
||||
import LibraryA
|
||||
import LibraryB
|
||||
import FrameworkA
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
@ -12,6 +13,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
|
||||
) -> Bool {
|
||||
|
||||
useFrameworkCode()
|
||||
usePackageCode()
|
||||
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
|
@ -22,6 +24,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
return true
|
||||
}
|
||||
|
||||
private func useFrameworkCode() {
|
||||
print(FrameworkAClass().text)
|
||||
}
|
||||
|
||||
private func usePackageCode() {
|
||||
print(LibraryAClass().text)
|
||||
print(LibraryBClass().text)
|
||||
|
|
Loading…
Reference in New Issue