Remove symbols from test input/output file lists (#1118)

When a project has a scheme that runs multiple test targets that rely on
the `Embed Precompiled Frameworks` script then the new build system
fails because there are multiple commands that produce the same target
file.

This commit prevents adding symbol files to the input/output file lists
for test targets to prevent this error from occuring.

Fixes tuist/tuist#919
This commit is contained in:
paul.s 2020-03-30 08:47:01 +01:00 committed by GitHub
parent 4bae7665ef
commit 79395e0430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 228 additions and 38 deletions

View File

@ -4,6 +4,10 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
## Next ## 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 ## 1.5.2
### Fixed ### Fixed

View File

@ -46,15 +46,6 @@
"version": "0.4.1" "version": "0.4.1"
} }
}, },
{
"package": "PathKit",
"repositoryURL": "https://github.com/kylef/PathKit.git",
"state": {
"branch": null,
"revision": "73f8e9dca9b7a3078cb79128217dc8f2e585a511",
"version": "1.0.0"
}
},
{ {
"package": "RxSwift", "package": "RxSwift",
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git", "repositoryURL": "https://github.com/ReactiveX/RxSwift.git",

View File

@ -89,6 +89,7 @@ final class LinkGenerator: LinkGenerating {
linkableModules: linkableModules) linkableModules: linkableModules)
try generateEmbedPhase(dependencies: embeddableFrameworks, try generateEmbedPhase(dependencies: embeddableFrameworks,
target: target,
pbxTarget: pbxTarget, pbxTarget: pbxTarget,
pbxproj: pbxproj, pbxproj: pbxproj,
fileElements: fileElements, fileElements: fileElements,
@ -152,6 +153,7 @@ final class LinkGenerator: LinkGenerating {
} }
func generateEmbedPhase(dependencies: [GraphDependencyReference], func generateEmbedPhase(dependencies: [GraphDependencyReference],
target: Target,
pbxTarget: PBXTarget, pbxTarget: PBXTarget,
pbxproj: PBXProj, pbxproj: PBXProj,
fileElements: ProjectFileElements, fileElements: ProjectFileElements,
@ -201,7 +203,10 @@ final class LinkGenerator: LinkGenerating {
if frameworkReferences.isEmpty { if frameworkReferences.isEmpty {
precompiledEmbedPhase.shellScript = "echo \"Skipping, nothing to be embedded.\"" precompiledEmbedPhase.shellScript = "echo \"Skipping, nothing to be embedded.\""
} else { } 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.shellScript = script.script
precompiledEmbedPhase.inputPaths = script.inputPaths.map(\.pathString) precompiledEmbedPhase.inputPaths = script.inputPaths.map(\.pathString)
precompiledEmbedPhase.outputPaths = script.outputPaths precompiledEmbedPhase.outputPaths = script.outputPaths

View File

@ -12,7 +12,10 @@ protocol EmbedScriptGenerating {
/// to embed the given frameworks into the compiled product. /// to embed the given frameworks into the compiled product.
/// - Parameter sourceRootPath: Directory where the Xcode project will be created. /// - Parameter sourceRootPath: Directory where the Xcode project will be created.
/// - Parameter frameworkReferences: Framework references. /// - 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. /// It represents a embed frameworks script.
@ -30,11 +33,13 @@ struct EmbedScript {
final class EmbedScriptGenerator: EmbedScriptGenerating { final class EmbedScriptGenerator: EmbedScriptGenerating {
typealias FrameworkScript = (script: String, inputPaths: [RelativePath], outputPaths: [String]) 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() var script = baseScript()
script.append("\n") 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) script.append(frameworksScript)
return EmbedScript(script: script, inputPaths: inputPaths, outputPaths: outputPaths) return EmbedScript(script: script, inputPaths: inputPaths, outputPaths: outputPaths)
@ -42,11 +47,25 @@ final class EmbedScriptGenerator: EmbedScriptGenerating {
// MARK: - Fileprivate // 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 script = ""
var inputPaths: [RelativePath] = [] var inputPaths: [RelativePath] = []
var outputPaths: [String] = [] 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 { for frameworkReference in frameworkReferences {
guard case let GraphDependencyReference.framework(path, _, _, dsymPath, bcsymbolmapPaths, _, _, _) = frameworkReference else { guard case let GraphDependencyReference.framework(path, _, _, dsymPath, bcsymbolmapPaths, _, _, _) = frameworkReference else {
preconditionFailure("references need to be of type framework") preconditionFailure("references need to be of type framework")
@ -63,16 +82,14 @@ final class EmbedScriptGenerator: EmbedScriptGenerating {
if let dsymPath = dsymPath { if let dsymPath = dsymPath {
let relativeDsymPath = dsymPath.relative(to: sourceRootPath) let relativeDsymPath = dsymPath.relative(to: sourceRootPath)
script.append("install_dsym \"\(relativeDsymPath.pathString)\"\n") script.append("install_dsym \"\(relativeDsymPath.pathString)\"\n")
inputPaths.append(relativeDsymPath) addSymbolsIfRequired(inputPath: relativeDsymPath, outputPath: "${DWARF_DSYM_FOLDER_PATH}/\(dsymPath.basename)")
outputPaths.append("${DWARF_DSYM_FOLDER_PATH}/\(dsymPath.basename)")
} }
// .bcsymbolmap // .bcsymbolmap
for bcsymbolmapPath in bcsymbolmapPaths { for bcsymbolmapPath in bcsymbolmapPaths {
let relativeDsymPath = bcsymbolmapPath.relative(to: sourceRootPath) let relativeDsymPath = bcsymbolmapPath.relative(to: sourceRootPath)
script.append("install_bcsymbolmap \"\(relativeDsymPath.pathString)\"\n") script.append("install_bcsymbolmap \"\(relativeDsymPath.pathString)\"\n")
inputPaths.append(relativeDsymPath) addSymbolsIfRequired(inputPath: relativeDsymPath, outputPath: "${BUILT_PRODUCTS_DIR}/\(relativeDsymPath.basename)")
outputPaths.append("${BUILT_PRODUCTS_DIR}/\(relativeDsymPath.basename)")
} }
} }
return (script: script, inputPaths: inputPaths, outputPaths: outputPaths) return (script: script, inputPaths: inputPaths, outputPaths: outputPaths)

View File

@ -40,7 +40,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
dependencies.append(GraphDependencyReference.testFramework()) dependencies.append(GraphDependencyReference.testFramework())
dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework"))
let pbxproj = PBXProj() let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test") let (pbxTarget, target) = createTargets(product: .framework)
let fileElements = ProjectFileElements() let fileElements = ProjectFileElements()
let wakaFile = PBXFileReference() let wakaFile = PBXFileReference()
pbxproj.add(object: wakaFile) pbxproj.add(object: wakaFile)
@ -52,6 +52,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
// When // When
try subject.generateEmbedPhase(dependencies: dependencies, try subject.generateEmbedPhase(dependencies: dependencies,
target: target,
pbxTarget: pbxTarget, pbxTarget: pbxTarget,
pbxproj: pbxproj, pbxproj: pbxproj,
fileElements: fileElements, fileElements: fileElements,
@ -72,15 +73,76 @@ final class LinkGeneratorErrorTests: XCTestCase {
XCTAssertEqual(settings, ["ATTRIBUTES": ["CodeSignOnCopy", "RemoveHeadersOnCopy"]]) 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 { func test_generateEmbedPhase_throws_when_aProductIsMissing() throws {
var dependencies: [GraphDependencyReference] = [] var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework")) dependencies.append(GraphDependencyReference.product(target: "Test", productName: "Test.framework"))
let pbxproj = PBXProj() let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test") let (pbxTarget, target) = createTargets(product: .framework)
let fileElements = ProjectFileElements() let fileElements = ProjectFileElements()
let sourceRootPath = AbsolutePath("/") let sourceRootPath = AbsolutePath("/")
XCTAssertThrowsError(try subject.generateEmbedPhase(dependencies: dependencies, XCTAssertThrowsError(try subject.generateEmbedPhase(dependencies: dependencies,
target: target,
pbxTarget: pbxTarget, pbxTarget: pbxTarget,
pbxproj: pbxproj, pbxproj: pbxproj,
fileElements: fileElements, fileElements: fileElements,
@ -94,7 +156,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
var dependencies: [GraphDependencyReference] = [] var dependencies: [GraphDependencyReference] = []
dependencies.append(GraphDependencyReference.testXCFramework(path: "/Frameworks/Test.xcframework")) dependencies.append(GraphDependencyReference.testXCFramework(path: "/Frameworks/Test.xcframework"))
let pbxproj = PBXProj() let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test") let (pbxTarget, target) = createTargets(product: .framework)
let sourceRootPath = AbsolutePath("/") let sourceRootPath = AbsolutePath("/")
let group = PBXGroup() let group = PBXGroup()
@ -105,6 +167,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
// When // When
try subject.generateEmbedPhase(dependencies: dependencies, try subject.generateEmbedPhase(dependencies: dependencies,
target: target,
pbxTarget: pbxTarget, pbxTarget: pbxTarget,
pbxproj: pbxproj, pbxproj: pbxproj,
fileElements: fileElements, fileElements: fileElements,
@ -539,6 +602,13 @@ final class LinkGeneratorErrorTests: XCTestCase {
return projectFileElements 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 { private func createFileElements(fileAbsolutePath: AbsolutePath) -> ProjectFileElements {
let fileElements = ProjectFileElements() let fileElements = ProjectFileElements()
fileElements.elements[fileAbsolutePath] = PBXFileReference(path: fileAbsolutePath.basename) fileElements.elements[fileAbsolutePath] = PBXFileReference(path: fileAbsolutePath.basename)

View File

@ -20,7 +20,7 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase {
subject = nil subject = nil
} }
func test_script() throws { func test_script_when_includingSymbolsInFileLists() throws {
// Given // Given
let path = AbsolutePath("/frameworks/tuist.framework") let path = AbsolutePath("/frameworks/tuist.framework")
let dsymPath = AbsolutePath("/frameworks/tuist.dSYM") let dsymPath = AbsolutePath("/frameworks/tuist.dSYM")
@ -30,7 +30,7 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase {
dsymPath: dsymPath, dsymPath: dsymPath,
bcsymbolmapPaths: [bcsymbolPath]) bcsymbolmapPaths: [bcsymbolPath])
// When // 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 // Then
XCTAssertEqual(got.inputPaths, [ XCTAssertEqual(got.inputPaths, [
@ -47,4 +47,29 @@ final class EmbedScriptGeneratorTests: TuistUnitTestCase {
XCTAssertTrue(got.script.contains("install_dsym \"\(dsymPath.basename)\"")) XCTAssertTrue(got.script.contains("install_dsym \"\(dsymPath.basename)\""))
XCTAssertTrue(got.script.contains("install_bcsymbolmap \"\(bcsymbolPath.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)\""))
}
} }

View File

@ -6,12 +6,13 @@ import TuistCore
@testable import TuistSupportTesting @testable import TuistSupportTesting
final class MockEmbedScriptGenerator: EmbedScriptGenerating { final class MockEmbedScriptGenerator: EmbedScriptGenerating {
var scriptArgs: [(AbsolutePath, [GraphDependencyReference])] = [] var scriptArgs: [(AbsolutePath, [GraphDependencyReference], Bool)] = []
var scriptStub: Result<EmbedScript, Error>? var scriptStub: Result<EmbedScript, Error>?
func script(sourceRootPath: AbsolutePath, func script(sourceRootPath: AbsolutePath,
frameworkReferences: [GraphDependencyReference]) throws -> EmbedScript { frameworkReferences: [GraphDependencyReference],
scriptArgs.append((sourceRootPath, frameworkReferences)) includeSymbolsInFileLists: Bool) throws -> EmbedScript {
scriptArgs.append((sourceRootPath, frameworkReferences, includeSymbolsInFileLists))
if let scriptStub = scriptStub { if let scriptStub = scriptStub {
switch scriptStub { switch scriptStub {
case let .failure(error): throw error case let .failure(error): throw error

View File

@ -30,7 +30,7 @@ Scenario: The project is an iOS application with Carthage frameworks (ios_app_wi
And I have a working directory And I have a working directory
Then I copy the fixture ios_app_with_carthage_frameworks into the working directory Then I copy the fixture ios_app_with_carthage_frameworks into the working directory
Then tuist generates the project 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' 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' contains the framework 'RxSwift' with architecture 'arm64'
Then the product 'App.app' with destination 'Debug-iphoneos' does not contain headers 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 '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' 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 Then the product 'App.app' with destination 'Debug-iphoneos' does not contain headers

View File

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

View File

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

View File

@ -2,14 +2,51 @@ import ProjectDescription
let project = Project(name: "App", let project = Project(name: "App",
targets: [ targets: [
Target(name: "App", Target(name: "App",
platform: .iOS, platform: .iOS,
product: .app, product: .app,
bundleId: "io.tuist.App", bundleId: "io.tuist.App",
infoPlist: .extendingDefault(with: [:]), infoPlist: .default,
sources: "Sources/**", sources: "Sources/**",
resources: "Sources/Main.storyboard", resources: "Sources/Main.storyboard",
dependencies: [ dependencies: [
.framework(path: "Carthage/Build/iOS/RxSwift.framework") .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"
]))
]) ])

View File

@ -8,4 +8,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidFinishLaunching(_: UIApplication) { func applicationDidFinishLaunching(_: UIApplication) {
let observable = Observable<String>.just("hello world") let observable = Observable<String>.just("hello world")
} }
public func hello() -> String {
return "AppDelegate.hello()"
}
} }

View File

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