diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1f85098..085a4e9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/ ## Next +### Fixed + +- Prevent `Multiple commands produce XXXXX` error produced by multiple test targets using “Embed Precompiled Frameworks” script https://github.com/tuist/tuist/pull/1118 by @paulsamuels + ## 1.5.2 ### Fixed diff --git a/Package.resolved b/Package.resolved index 229296294..aab3ffeaf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -46,15 +46,6 @@ "version": "0.4.1" } }, - { - "package": "PathKit", - "repositoryURL": "https://github.com/kylef/PathKit.git", - "state": { - "branch": null, - "revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511", - "version": "1.0.0" - } - }, { "package": "RxSwift", "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", diff --git a/Sources/TuistGenerator/Generator/LinkGenerator.swift b/Sources/TuistGenerator/Generator/LinkGenerator.swift index 034514d6c..798ba7a40 100644 --- a/Sources/TuistGenerator/Generator/LinkGenerator.swift +++ b/Sources/TuistGenerator/Generator/LinkGenerator.swift @@ -89,6 +89,7 @@ final class LinkGenerator: LinkGenerating { linkableModules: linkableModules) try generateEmbedPhase(dependencies: embeddableFrameworks, + target: target, pbxTarget: pbxTarget, pbxproj: pbxproj, fileElements: fileElements, @@ -152,6 +153,7 @@ final class LinkGenerator: LinkGenerating { } func generateEmbedPhase(dependencies: [GraphDependencyReference], + target: Target, pbxTarget: PBXTarget, pbxproj: PBXProj, fileElements: ProjectFileElements, @@ -201,7 +203,10 @@ final class LinkGenerator: LinkGenerating { if frameworkReferences.isEmpty { precompiledEmbedPhase.shellScript = "echo \"Skipping, nothing to be embedded.\"" } else { - let script = try embedScriptGenerator.script(sourceRootPath: sourceRootPath, frameworkReferences: frameworkReferences) + let script = try embedScriptGenerator.script(sourceRootPath: sourceRootPath, + frameworkReferences: frameworkReferences, + includeSymbolsInFileLists: !target.product.testsBundle) + precompiledEmbedPhase.shellScript = script.script precompiledEmbedPhase.inputPaths = script.inputPaths.map(\.pathString) precompiledEmbedPhase.outputPaths = script.outputPaths diff --git a/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift b/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift index 772b5b253..ad2c78ba7 100644 --- a/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift +++ b/Sources/TuistGenerator/Utils/EmbedScriptGenerator.swift @@ -12,7 +12,10 @@ protocol EmbedScriptGenerating { /// to embed the given frameworks into the compiled product. /// - Parameter sourceRootPath: Directory where the Xcode project will be created. /// - Parameter frameworkReferences: Framework references. - func script(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript + /// - Parameter includeSymbolsInFileLists: Whether or not to list DSYMs/bcsymbol files in the input/output file list. + func script(sourceRootPath: AbsolutePath, + frameworkReferences: [GraphDependencyReference], + includeSymbolsInFileLists: Bool) throws -> EmbedScript } /// It represents a embed frameworks script. @@ -30,11 +33,13 @@ struct EmbedScript { final class EmbedScriptGenerator: EmbedScriptGenerating { typealias FrameworkScript = (script: String, inputPaths: [RelativePath], outputPaths: [String]) - func script(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript { + func script(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference], includeSymbolsInFileLists: Bool) throws -> EmbedScript { var script = baseScript() script.append("\n") - let (frameworksScript, inputPaths, outputPaths) = try self.frameworksScript(sourceRootPath: sourceRootPath, frameworkReferences: frameworkReferences) + let (frameworksScript, inputPaths, outputPaths) = try self.frameworksScript(sourceRootPath: sourceRootPath, + frameworkReferences: frameworkReferences, + includeSymbolsInFileLists: includeSymbolsInFileLists) script.append(frameworksScript) return EmbedScript(script: script, inputPaths: inputPaths, outputPaths: outputPaths) @@ -42,11 +47,25 @@ final class EmbedScriptGenerator: EmbedScriptGenerating { // MARK: - Fileprivate - fileprivate func frameworksScript(sourceRootPath: AbsolutePath, frameworkReferences: [GraphDependencyReference]) throws -> FrameworkScript { + fileprivate func frameworksScript(sourceRootPath: AbsolutePath, + frameworkReferences: [GraphDependencyReference], + includeSymbolsInFileLists: Bool) throws -> FrameworkScript { var script = "" var inputPaths: [RelativePath] = [] var outputPaths: [String] = [] + /* + * This is required to prevent the new build system from failing when multiple test targets cause the same + * output files. The symbols aren't really required to be copied for tests so this patch excludes them. + * For more details see the discussion on https://github.com/tuist/tuist/issues/919. + */ + func addSymbolsIfRequired(inputPath: RelativePath, outputPath: String) { + if includeSymbolsInFileLists { + inputPaths.append(inputPath) + outputPaths.append(outputPath) + } + } + for frameworkReference in frameworkReferences { guard case let GraphDependencyReference.framework(path, _, _, dsymPath, bcsymbolmapPaths, _, _, _) = frameworkReference else { preconditionFailure("references need to be of type framework") @@ -63,16 +82,14 @@ final class EmbedScriptGenerator: EmbedScriptGenerating { if let dsymPath = dsymPath { let relativeDsymPath = dsymPath.relative(to: sourceRootPath) script.append("install_dsym \"\(relativeDsymPath.pathString)\"\n") - inputPaths.append(relativeDsymPath) - outputPaths.append("${DWARF_DSYM_FOLDER_PATH}/\(dsymPath.basename)") + addSymbolsIfRequired(inputPath: relativeDsymPath, outputPath: "${DWARF_DSYM_FOLDER_PATH}/\(dsymPath.basename)") } // .bcsymbolmap for bcsymbolmapPath in bcsymbolmapPaths { let relativeDsymPath = bcsymbolmapPath.relative(to: sourceRootPath) script.append("install_bcsymbolmap \"\(relativeDsymPath.pathString)\"\n") - inputPaths.append(relativeDsymPath) - outputPaths.append("${BUILT_PRODUCTS_DIR}/\(relativeDsymPath.basename)") + addSymbolsIfRequired(inputPath: relativeDsymPath, outputPath: "${BUILT_PRODUCTS_DIR}/\(relativeDsymPath.basename)") } } return (script: script, inputPaths: inputPaths, outputPaths: outputPaths) diff --git a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift index e7ecc4a03..68f9bf8a3 100644 --- a/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Generator/LinkGeneratorTests.swift @@ -40,7 +40,7 @@ final class LinkGeneratorErrorTests: XCTestCase { dependencies.append(GraphDependencyReference.testFramework()) dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) let pbxproj = PBXProj() - let pbxTarget = PBXNativeTarget(name: "Test") + let (pbxTarget, target) = createTargets(product: .framework) let fileElements = ProjectFileElements() let wakaFile = PBXFileReference() pbxproj.add(object: wakaFile) @@ -52,6 +52,7 @@ final class LinkGeneratorErrorTests: XCTestCase { // When try subject.generateEmbedPhase(dependencies: dependencies, + target: target, pbxTarget: pbxTarget, pbxproj: pbxproj, fileElements: fileElements, @@ -72,15 +73,76 @@ final class LinkGeneratorErrorTests: XCTestCase { XCTAssertEqual(settings, ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]]) } + func test_generateEmbedPhase_includesSymbols_when_nonTestTarget() throws { + try Product.allCases.filter { !$0.testsBundle }.forEach { product in + // Given + + var dependencies: [GraphDependencyReference] = [] + dependencies.append(GraphDependencyReference.testFramework()) + dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) + let pbxproj = PBXProj() + let (pbxTarget, target) = createTargets(product: product) + let fileElements = ProjectFileElements() + let wakaFile = PBXFileReference() + pbxproj.add(object: wakaFile) + fileElements.products["Test"] = wakaFile + let sourceRootPath = AbsolutePath("/") + embedScriptGenerator.scriptStub = .success(EmbedScript(script: "script", + inputPaths: [RelativePath("frameworks/A.framework")], + outputPaths: ["output/A.framework"])) + + // When + try subject.generateEmbedPhase(dependencies: dependencies, + target: target, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + fileElements: fileElements, + sourceRootPath: sourceRootPath) + + XCTAssert(embedScriptGenerator.scriptArgs.last?.2 == true, "Expected `includeSymbolsInFileLists == true` for product `\(product)`") + } + } + + func test_generateEmbedPhase_doesNot_includesSymbols_when_testTarget() throws { + try Product.allCases.filter { $0.testsBundle }.forEach { product in + // Given + + var dependencies: [GraphDependencyReference] = [] + dependencies.append(GraphDependencyReference.testFramework()) + dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) + let pbxproj = PBXProj() + let (pbxTarget, target) = createTargets(product: product) + let fileElements = ProjectFileElements() + let wakaFile = PBXFileReference() + pbxproj.add(object: wakaFile) + fileElements.products["Test"] = wakaFile + let sourceRootPath = AbsolutePath("/") + embedScriptGenerator.scriptStub = .success(EmbedScript(script: "script", + inputPaths: [RelativePath("frameworks/A.framework")], + outputPaths: ["output/A.framework"])) + + // When + try subject.generateEmbedPhase(dependencies: dependencies, + target: target, + pbxTarget: pbxTarget, + pbxproj: pbxproj, + fileElements: fileElements, + sourceRootPath: sourceRootPath) + + XCTAssert(embedScriptGenerator.scriptArgs.last?.2 == false, "Expected `includeSymbolsInFileLists == false` for product `\(product)`") + } + } + func test_generateEmbedPhase_throws_when_aProductIsMissing() throws { var dependencies: [GraphDependencyReference] = [] dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) let pbxproj = PBXProj() - let pbxTarget = PBXNativeTarget(name: "Test") + let (pbxTarget, target) = createTargets(product: .framework) let fileElements = ProjectFileElements() let sourceRootPath = AbsolutePath("/") XCTAssertThrowsError(try subject.generateEmbedPhase(dependencies: dependencies, + target: target, pbxTarget: pbxTarget, pbxproj: pbxproj, fileElements: fileElements, @@ -94,7 +156,7 @@ final class LinkGeneratorErrorTests: XCTestCase { var dependencies: [GraphDependencyReference] = [] dependencies.append(GraphDependencyReference.testXCFramework(path: "/Frameworks/Test.xcframework")) let pbxproj = PBXProj() - let pbxTarget = PBXNativeTarget(name: "Test") + let (pbxTarget, target) = createTargets(product: .framework) let sourceRootPath = AbsolutePath("/") let group = PBXGroup() @@ -105,6 +167,7 @@ final class LinkGeneratorErrorTests: XCTestCase { // When try subject.generateEmbedPhase(dependencies: dependencies, + target: target, pbxTarget: pbxTarget, pbxproj: pbxproj, fileElements: fileElements, @@ -539,6 +602,13 @@ final class LinkGeneratorErrorTests: XCTestCase { return projectFileElements } + private func createTargets(product: Product) -> (PBXTarget, Target) { + return ( + PBXNativeTarget(name: "Test"), + Target.test(name: "Test", product: product) + ) + } + private func createFileElements(fileAbsolutePath: AbsolutePath) -> ProjectFileElements { let fileElements = ProjectFileElements() fileElements.elements[fileAbsolutePath] = PBXFileReference(path: fileAbsolutePath.basename) diff --git a/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift b/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift index cf3940813..7dde95b63 100644 --- a/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift +++ b/Tests/TuistGeneratorTests/Utils/EmbedScriptGeneratorTests.swift @@ -20,7 +20,7 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase { subject = nil } - func test_script() throws { + func test_script_when_includingSymbolsInFileLists() throws { // Given let path = AbsolutePath("/frameworks/tuist.framework") let dsymPath = AbsolutePath("/frameworks/tuist.dSYM") @@ -30,7 +30,7 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase { dsymPath: dsymPath, bcsymbolmapPaths: [bcsymbolPath]) // When - let got = try subject.script(sourceRootPath: framework.precompiledPath!.parentDirectory, frameworkReferences: [framework]) + let got = try subject.script(sourceRootPath: framework.precompiledPath!.parentDirectory, frameworkReferences: [framework], includeSymbolsInFileLists: true) // Then XCTAssertEqual(got.inputPaths, [ @@ -47,4 +47,29 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase { XCTAssertTrue(got.script.contains("install_dsym \"\(dsymPath.basename)\"")) XCTAssertTrue(got.script.contains("install_bcsymbolmap \"\(bcsymbolPath.basename)\"")) } + + func test_script_when_not_includingSymbolsInFileLists() throws { + // Given + let path = AbsolutePath("/frameworks/tuist.framework") + let dsymPath = AbsolutePath("/frameworks/tuist.dSYM") + let bcsymbolPath = AbsolutePath("/frameworks/tuist.bcsymbolmap") + let framework = GraphDependencyReference.testFramework(path: path, + binaryPath: path.appending(component: "tuist"), + dsymPath: dsymPath, + bcsymbolmapPaths: [bcsymbolPath]) + // When + let got = try subject.script(sourceRootPath: framework.precompiledPath!.parentDirectory, frameworkReferences: [framework], includeSymbolsInFileLists: false) + + // Then + XCTAssertEqual(got.inputPaths, [ + RelativePath(path.basename), + ]) + XCTAssertEqual(got.outputPaths, [ + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/\(path.basename)", + ]) + + XCTAssertTrue(got.script.contains("install_framework \"\(path.basename)\"")) + XCTAssertTrue(got.script.contains("install_dsym \"\(dsymPath.basename)\"")) + XCTAssertTrue(got.script.contains("install_bcsymbolmap \"\(bcsymbolPath.basename)\"")) + } } diff --git a/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift b/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift index ff79d7da0..6571a28c7 100644 --- a/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift +++ b/Tests/TuistGeneratorTests/Utils/Mocks/MockEmbedScriptGenerator.swift @@ -6,12 +6,13 @@ import TuistCore @testable import TuistSupportTesting final class MockEmbedScriptGenerator: EmbedScriptGenerating { - var scriptArgs: [(AbsolutePath, [GraphDependencyReference])] = [] + var scriptArgs: [(AbsolutePath, [GraphDependencyReference], Bool)] = [] var scriptStub: Result? func script(sourceRootPath: AbsolutePath, - frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript { - scriptArgs.append((sourceRootPath, frameworkReferences)) + frameworkReferences: [GraphDependencyReference], + includeSymbolsInFileLists: Bool) throws -> EmbedScript { + scriptArgs.append((sourceRootPath, frameworkReferences, includeSymbolsInFileLists)) if let scriptStub = scriptStub { switch scriptStub { case let .failure(error): throw error diff --git a/features/generate-4.feature b/features/generate-4.feature index 3b45b2cd9..05d677f5d 100644 --- a/features/generate-4.feature +++ b/features/generate-4.feature @@ -30,7 +30,7 @@ Scenario: The project is an iOS application with Carthage frameworks (ios_app_wi And I have a working directory Then I copy the fixture ios_app_with_carthage_frameworks into the working directory Then tuist generates the project - Then I should be able to build for iOS the scheme App + Then I should be able to build for iOS the scheme AllTargets Then the product 'App.app' with destination 'Debug-iphoneos' contains the framework 'RxSwift' without architecture 'armv7' Then the product 'App.app' with destination 'Debug-iphoneos' contains the framework 'RxSwift' with architecture 'arm64' Then the product 'App.app' with destination 'Debug-iphoneos' does not contain headers @@ -44,4 +44,4 @@ Scenario: The project is an iOS application with extensions (ios_app_with_extens Then the product 'App.app' with destination 'Debug-iphoneos' contains extension 'StickersPackExtension' Then the product 'App.app' with destination 'Debug-iphoneos' contains extension 'NotificationServiceExtension' Then the product 'App.app' with destination 'Debug-iphoneos' contains extension 'NotificationServiceExtension' - Then the product 'App.app' with destination 'Debug-iphoneos' does not contain headers \ No newline at end of file + Then the product 'App.app' with destination 'Debug-iphoneos' does not contain headers diff --git a/fixtures/ios_app_with_carthage_frameworks/Core/CoreFile.swift b/fixtures/ios_app_with_carthage_frameworks/Core/CoreFile.swift new file mode 100644 index 000000000..a6beae394 --- /dev/null +++ b/fixtures/ios_app_with_carthage_frameworks/Core/CoreFile.swift @@ -0,0 +1,9 @@ +import Foundation + +public class CoreFile { + public init() {} + + public func hello() -> String { + return "CoreFile.hello()" + } +} diff --git a/fixtures/ios_app_with_carthage_frameworks/CoreTests/CoreTests.swift b/fixtures/ios_app_with_carthage_frameworks/CoreTests/CoreTests.swift new file mode 100644 index 000000000..45e2484c7 --- /dev/null +++ b/fixtures/ios_app_with_carthage_frameworks/CoreTests/CoreTests.swift @@ -0,0 +1,14 @@ +import Foundation +import XCTest + +@testable import Core + +final class CoreTests: XCTestCase { + + func testHello() { + let sut = CoreFile() + + XCTAssertEqual("CoreTests.hello()", sut.hello()) + } + +} diff --git a/fixtures/ios_app_with_carthage_frameworks/Project.swift b/fixtures/ios_app_with_carthage_frameworks/Project.swift index 07e158e56..556e2c88e 100644 --- a/fixtures/ios_app_with_carthage_frameworks/Project.swift +++ b/fixtures/ios_app_with_carthage_frameworks/Project.swift @@ -2,14 +2,51 @@ import ProjectDescription let project = Project(name: "App", targets: [ - Target(name: "App", - platform: .iOS, - product: .app, - bundleId: "io.tuist.App", - infoPlist: .extendingDefault(with: [:]), - sources: "Sources/**", - resources: "Sources/Main.storyboard", - dependencies: [ - .framework(path: "Carthage/Build/iOS/RxSwift.framework") - ]) + Target(name: "App", + platform: .iOS, + product: .app, + bundleId: "io.tuist.App", + infoPlist: .default, + sources: "Sources/**", + resources: "Sources/Main.storyboard", + dependencies: [ + .target(name: "Core"), + .framework(path: "Carthage/Build/iOS/RxSwift.framework") + ]), + Target(name: "AppTests", + platform: .iOS, + product: .unitTests, + bundleId: "io.tuist.AppTests", + infoPlist: .default, + sources: "Tests/**", + dependencies: [ + .target(name: "App"), + ]), + Target(name: "Core", + platform: .iOS, + product: .framework, + bundleId: "io.tuist.Core", + infoPlist: .default, + sources: "Core/**", + dependencies: [ + .framework(path: "Carthage/Build/iOS/RxSwift.framework"), + ]), + Target(name: "CoreTests", + platform: .iOS, + product: .unitTests, + bundleId: "io.tuist.CoreTests", + infoPlist: .default, + sources: "CoreTests/**", + dependencies: [ + .target(name: "Core") + ]) + ], schemes: [ + Scheme(name: "AllTargets", + shared: true, + buildAction: BuildAction(targets: [ + "App", + "AppTests", + "Core", + "CoreTests" + ])) ]) diff --git a/fixtures/ios_app_with_carthage_frameworks/Sources/AppDelegate.swift b/fixtures/ios_app_with_carthage_frameworks/Sources/AppDelegate.swift index e9ccbf3f2..b96d7b88e 100644 --- a/fixtures/ios_app_with_carthage_frameworks/Sources/AppDelegate.swift +++ b/fixtures/ios_app_with_carthage_frameworks/Sources/AppDelegate.swift @@ -8,4 +8,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidFinishLaunching(_: UIApplication) { let observable = Observable.just("hello world") } + + public func hello() -> String { + return "AppDelegate.hello()" + } } diff --git a/fixtures/ios_app_with_carthage_frameworks/Tests/AppTests.swift b/fixtures/ios_app_with_carthage_frameworks/Tests/AppTests.swift new file mode 100644 index 000000000..780a33480 --- /dev/null +++ b/fixtures/ios_app_with_carthage_frameworks/Tests/AppTests.swift @@ -0,0 +1,13 @@ +import Foundation +import XCTest + +@testable import App + +final class AppTests: XCTestCase { + + func testHello() { + let sut = AppDelegate() + + XCTAssertEqual("AppDelegate.hello()", sut.hello()) + } +}