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:
Kas 2019-10-03 21:51:09 +01:00 committed by GitHub
parent 11da16b36f
commit b6c822d520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 496 additions and 275 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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? {

View File

@ -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? {

View File

@ -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()

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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")
]),
]
)

View File

@ -0,0 +1,9 @@
import Foundation
public class Framework3File {
public init() {}
public func hello() -> String {
return "Framework3File.hello()"
}
}

View File

@ -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>

View File

@ -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")
]),
]
)

View File

@ -0,0 +1,9 @@
import Foundation
public class Framework4File {
public init() {}
public func hello() -> String {
return "Framework4File.hello()"
}
}

View File

@ -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>

View File

@ -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")
]),
]
)

View File

@ -0,0 +1,9 @@
import Foundation
public class Framework5File {
public init() {}
public func hello() -> String {
return "Framework5File.hello()"
}
}

View File

@ -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>

View File

@ -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"),
]),
]
)

View File

@ -0,0 +1,10 @@
import Foundation
import LibraryA
public class FrameworkAClass {
public let text: String
public init() {
let libraryAClass = LibraryAClass()
text = "FrameworkAClass::\(libraryAClass.text)"
}
}

View File

@ -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"),
]),

View File

@ -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)