diff --git a/Sources/ProjectSpec/AggregateTarget.swift b/Sources/ProjectSpec/AggregateTarget.swift index 1a3225c40..f6b65535d 100644 --- a/Sources/ProjectSpec/AggregateTarget.swift +++ b/Sources/ProjectSpec/AggregateTarget.swift @@ -12,6 +12,7 @@ public struct AggregateTarget: ProjectTarget { public var configFiles: [String: String] public var scheme: TargetScheme? public var attributes: [String: Any] + public var nameDividerChar: String = Target.defaultNameDividerChar public init( name: String, diff --git a/Sources/ProjectSpec/ProjectTarget.swift b/Sources/ProjectSpec/ProjectTarget.swift index 6539f3e84..087f61343 100644 --- a/Sources/ProjectSpec/ProjectTarget.swift +++ b/Sources/ProjectSpec/ProjectTarget.swift @@ -9,6 +9,7 @@ public protocol ProjectTarget: BuildSettingsContainer { var buildToolPlugins: [BuildToolPlugin] { get } var scheme: TargetScheme? { get } var attributes: [String: Any] { get } + var nameDividerChar: String? { get } } extension Target { diff --git a/Sources/ProjectSpec/Target.swift b/Sources/ProjectSpec/Target.swift index 3b9f2c535..b0bfa5b86 100644 --- a/Sources/ProjectSpec/Target.swift +++ b/Sources/ProjectSpec/Target.swift @@ -34,7 +34,11 @@ extension LegacyTarget: PathContainer { } public struct Target: ProjectTarget { + + public static var defaultNameDividerChar = "_" + public var name: String + public var nameDividerChar: String public var type: PBXProductType public var platform: Platform public var supportedDestinations: [SupportedDestination]? @@ -77,6 +81,7 @@ public struct Target: ProjectTarget { public init( name: String, + nameDividerChar: String = Target.defaultNameDividerChar, type: PBXProductType, platform: Platform, supportedDestinations: [SupportedDestination]? = nil, @@ -103,6 +108,7 @@ public struct Target: ProjectTarget { putResourcesBeforeSourcesBuildPhase: Bool = false ) { self.name = name + self.nameDividerChar = nameDividerChar self.type = type self.platform = platform self.supportedDestinations = supportedDestinations @@ -272,6 +278,8 @@ extension Target: NamedJSONDictionaryConvertible { public init(name: String, jsonDictionary: JSONDictionary) throws { let resolvedName: String = jsonDictionary.json(atKeyPath: "name") ?? name self.name = resolvedName + self.nameDividerChar = jsonDictionary.json(atKeyPath: "nameDividerChar") + ?? Target.defaultNameDividerChar productName = jsonDictionary.json(atKeyPath: "productName") ?? resolvedName let typeString: String = jsonDictionary.json(atKeyPath: "type") ?? "" diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index f5932c837..2371f07d3 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -15,6 +15,7 @@ public struct TargetSource: Equatable { public var group: String? public var compilerFlags: [String] public var excludes: [String] + public var excludePatterns: [NSRegularExpression] public var includes: [String] public var type: SourceType? public var optional: Bool @@ -46,6 +47,7 @@ public struct TargetSource: Equatable { group: String? = nil, compilerFlags: [String] = [], excludes: [String] = [], + excludePatterns: [NSRegularExpression] = [], includes: [String] = [], type: SourceType? = nil, optional: Bool = optionalDefault, @@ -62,6 +64,7 @@ public struct TargetSource: Equatable { self.group = group self.compilerFlags = compilerFlags self.excludes = excludes + self.excludePatterns = excludePatterns self.includes = includes self.type = type self.optional = optional @@ -105,6 +108,10 @@ extension TargetSource: JSONObjectConvertible { headerVisibility = jsonDictionary.json(atKeyPath: "headerVisibility") excludes = jsonDictionary.json(atKeyPath: "excludes") ?? [] + let regexPatterns: [String] = jsonDictionary.json(atKeyPath: "excludePatterns") ?? [] + excludePatterns = try regexPatterns.map({ + try NSRegularExpression(pattern: $0) + }) includes = jsonDictionary.json(atKeyPath: "includes") ?? [] type = jsonDictionary.json(atKeyPath: "type") optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index a5f667afd..f5b8855d1 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -682,7 +682,7 @@ public class PBXProjGenerator { let infoPlistFiles: [Config: String] = getInfoPlists(for: target) let sourceFileBuildPhaseOverrideSequence: [(Path, BuildPhaseSpec)] = Set(infoPlistFiles.values).map({ (project.basePath + $0, .none) }) let sourceFileBuildPhaseOverrides = Dictionary(uniqueKeysWithValues: sourceFileBuildPhaseOverrideSequence) - let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources, buildPhases: sourceFileBuildPhaseOverrides) + let sourceFiles = try sourceGenerator.getAllSourceFiles(targetType: target.type, sources: target.sources, platform: target.platform, buildPhases: sourceFileBuildPhaseOverrides) .sorted { $0.path.lastComponent < $1.path.lastComponent } var anyDependencyRequiresObjCLinking = false diff --git a/Sources/XcodeGenKit/SchemeGenerator.swift b/Sources/XcodeGenKit/SchemeGenerator.swift index ac100e89f..10afa17da 100644 --- a/Sources/XcodeGenKit/SchemeGenerator.swift +++ b/Sources/XcodeGenKit/SchemeGenerator.swift @@ -71,7 +71,8 @@ public class SchemeGenerator { } else { for configVariant in targetScheme.configVariants { - let schemeName = "\(target.name) \(configVariant)" + let divider = target.nameDividerChar ?? Target.defaultNameDividerChar + let schemeName = "\(target.name)\(divider)\(configVariant)" let debugConfig = project.configs .first(including: configVariant, for: .debug)! diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 7ed90b3d6..9be8c0d50 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -22,6 +22,7 @@ class SourceGenerator { private let project: Project let pbxProj: PBXProj + var excludePatterns: [NSRegularExpression] = [] private var defaultExcludedFiles = [ ".DS_Store", ] @@ -86,8 +87,16 @@ class SourceGenerator { /// - targetType: The type of target that the source files should belong to. /// - sources: The array of sources defined as part of the targets spec. /// - buildPhases: A dictionary containing any build phases that should be applied to source files at specific paths in the event that the associated `TargetSource` didn't already define a `buildPhase`. Values from this dictionary are used in cases where the project generator knows more about a file than the spec/filesystem does (i.e if the file should be treated as the targets Info.plist and so on). - func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] { - try sources.flatMap { try getSourceFiles(targetType: targetType, targetSource: $0, buildPhases: buildPhases) } + func getAllSourceFiles(targetType: PBXProductType, sources: [TargetSource], platform: Platform, buildPhases: [Path : BuildPhaseSpec]) throws -> [SourceFile] { + try sources + .flatMap { + try getSourceFiles( + targetType: targetType, + targetSource: $0, + platform: platform, + buildPhases: buildPhases + ) + } } // get groups without build files. Use for Project.fileGroups @@ -393,12 +402,25 @@ class SourceGenerator { .reduce([], +) ) } + + func isExcludedPattern(_ path: Path) -> Bool { + return excludePatterns.reduce(false) { + (result: Bool, expression: NSRegularExpression) -> Bool in + + let string: String = path.string + let range = NSRange(location: 0, length: string.count) + let matches = expression.matches(in: string, range: range) + + return result || (matches.count > 0) + } + } /// Checks whether the path is not in any default or TargetSource excludes func isIncludedPath(_ path: Path, excludePaths: Set, includePaths: SortedArray?) -> Bool { return !defaultExcludedFiles.contains(where: { path.lastComponent == $0 }) && !(path.extension.map(defaultExcludedExtensions.contains) ?? false) && !excludePaths.contains(path) + && !isExcludedPattern(path) // If includes is empty, it's included. If it's not empty, the path either needs to match exactly, or it needs to be a direct parent of an included path. && (includePaths.flatMap { _isIncludedPathSorted(path, sortedPaths: $0) } ?? true) } @@ -599,13 +621,28 @@ class SourceGenerator { groups.insert(group, at: 0) return (allSourceFiles, groups) } + + private func excludePatternsForPlatform(_ platform: Platform) throws + -> NSRegularExpression { + + let pattern = "\\/\(platform.rawValue)\\/" + return try NSRegularExpression(pattern: pattern) + } /// creates source files - private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { + private func getSourceFiles(targetType: PBXProductType, targetSource: TargetSource, platform: Platform? = nil, buildPhases: [Path: BuildPhaseSpec]) throws -> [SourceFile] { // generate excluded paths let path = project.basePath + targetSource.path let excludePaths = getSourceMatches(targetSource: targetSource, patterns: targetSource.excludes) + excludePatterns = targetSource.excludePatterns + if let platform = platform { + var platforms = Set(Platform.allCases) + platforms.remove(platform) + excludePatterns += try platforms.map({ + try excludePatternsForPlatform($0) + }) + } // generate included paths. Excluded paths will override this. let includePaths = targetSource.includes.isEmpty ? nil : getSourceMatches(targetSource: targetSource, patterns: targetSource.includes) diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Production.xcscheme b/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Production.xcscheme similarity index 100% rename from Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Production.xcscheme rename to Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Production.xcscheme diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Staging.xcscheme b/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Staging.xcscheme similarity index 100% rename from Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Staging.xcscheme rename to Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Staging.xcscheme diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Test.xcscheme b/Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Test.xcscheme similarity index 100% rename from Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS Test.xcscheme rename to Tests/Fixtures/TestProject/Project.xcodeproj/xcshareddata/xcschemes/App_iOS_Test.xcscheme diff --git a/Tests/Fixtures/TestProject/build.sh b/Tests/Fixtures/TestProject/build.sh index 590d02387..9f9ef02a3 100755 --- a/Tests/Fixtures/TestProject/build.sh +++ b/Tests/Fixtures/TestProject/build.sh @@ -14,7 +14,7 @@ XCODE_XCCONFIG_FILE="$PWD/carthage_static.xcconfig" \ echo " ⚙️ Building iOS app" -xcodebuild -quiet -workspace Workspace.xcworkspace -scheme "App_iOS Test" -configuration "Test Debug" -xcconfig fixtures.xcconfig +xcodebuild -quiet -workspace Workspace.xcworkspace -scheme "App_iOS_Test" -configuration "Test Debug" -xcconfig fixtures.xcconfig echo "✅ Successfully built iOS app" echo " diff --git a/Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig b/Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig index 1622037ab..afb18d0b0 100644 --- a/Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig +++ b/Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig @@ -7,5 +7,8 @@ EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 EXCLUDED_ARCHS_1200=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) EXCLUDED_ARCHS_1300=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) +<<<<<<<< HEAD:Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig EXCLUDED_ARCHS_1400=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) +======== +>>>>>>>> 122aa7fc (IOS-5280 Align with main repo (#6)):Tests/Fixtures/TestProject/xcode12_and_13_workaround.xcconfig EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS_$(XCODE_VERSION_MAJOR)) diff --git a/Tests/Fixtures/TestProject/xcode12_and_13_workaround.xcconfig b/Tests/Fixtures/TestProject/xcode12_and_13_workaround.xcconfig new file mode 100644 index 000000000..afb18d0b0 --- /dev/null +++ b/Tests/Fixtures/TestProject/xcode12_and_13_workaround.xcconfig @@ -0,0 +1,14 @@ +// +// See https://github.com/Carthage/Carthage/issues/3019 +// +// Skips building ARM slices for simulators until Carthage can support it +// + +EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 +EXCLUDED_ARCHS_1200=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) +EXCLUDED_ARCHS_1300=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) +<<<<<<<< HEAD:Tests/Fixtures/TestProject/xcode12_13_and_14_workaround.xcconfig +EXCLUDED_ARCHS_1400=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)) +======== +>>>>>>>> 122aa7fc (IOS-5280 Align with main repo (#6)):Tests/Fixtures/TestProject/xcode12_and_13_workaround.xcconfig +EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS_$(XCODE_VERSION_MAJOR)) diff --git a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift index b067fa564..2dc7f35c6 100644 --- a/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/PBXProjGeneratorTests.swift @@ -504,5 +504,158 @@ class PBXProjGeneratorTests: XCTestCase { } } } + + func testDefaultLastUpgradeCheckWhenUserDidSpecifyInvalidValue() throws { + let lastUpgradeKey = "LastUpgradeCheck" + let attributes: [String: Any] = [lastUpgradeKey: 1234] + let project = Project(name: "Test", attributes: attributes) + let projGenerator = PBXProjGenerator(project: project) + + let pbxProj = try projGenerator.generate() + + for pbxProject in pbxProj.projects { + XCTAssertEqual(pbxProject.attributes[lastUpgradeKey] as? String, project.xcodeVersion) + } + } + + func testOverrideLastUpgradeCheckWhenUserDidSpecifyValue() throws { + let lastUpgradeKey = "LastUpgradeCheck" + let lastUpgradeValue = "1234" + let attributes: [String: Any] = [lastUpgradeKey: lastUpgradeValue] + let project = Project(name: "Test", attributes: attributes) + let projGenerator = PBXProjGenerator(project: project) + + let pbxProj = try projGenerator.generate() + + for pbxProject in pbxProj.projects { + XCTAssertEqual(pbxProject.attributes[lastUpgradeKey] as? String, lastUpgradeValue) + } + } + + func testDefaultLastUpgradeCheckWhenUserDidNotSpecifyValue() throws { + let lastUpgradeKey = "LastUpgradeCheck" + let project = Project(name: "Test") + let projGenerator = PBXProjGenerator(project: project) + + let pbxProj = try projGenerator.generate() + + for pbxProject in pbxProj.projects { + XCTAssertEqual(pbxProject.attributes[lastUpgradeKey] as? String, project.xcodeVersion) + } + } + + func testPlatformDependencies() { + describe { + let directoryPath = Path("TestDirectory") + + func createDirectories(_ directories: String) throws { + let yaml = try Yams.load(yaml: directories)! + + func getFiles(_ file: Any, path: Path) -> [Path] { + if let array = file as? [Any] { + return array.flatMap { getFiles($0, path: path) } + } else if let string = file as? String { + return [path + string] + } else if let dictionary = file as? [String: Any] { + var array: [Path] = [] + for (key, value) in dictionary { + array += getFiles(value, path: path + key) + } + return array + } else { + return [] + } + } + + let files = getFiles(yaml, path: directoryPath).filter { $0.extension != nil } + for file in files { + try file.parent().mkpath() + try file.write("") + } + } + + func removeDirectories() { + try? directoryPath.delete() + } + + $0.before { + removeDirectories() + } + + $0.after { + removeDirectories() + } + + $0.it("setups target with different dependencies") { + let directories = """ + Sources: + - MainScreen: + - Entities: + - file.swift + """ + try createDirectories(directories) + let target1 = Target(name: "TestAll", type: .application, platform: .iOS, sources: ["Sources"]) + let target2 = Target(name: "TestiOS", type: .application, platform: .iOS, sources: ["Sources"]) + let target3 = Target(name: "TestmacOS", type: .application, platform: .iOS, sources: ["Sources"]) + let dependency1 = Dependency(type: .target, reference: "TestAll", platformFilter: .all) + let dependency2 = Dependency(type: .target, reference: "TestiOS", platformFilter: .iOS) + let dependency3 = Dependency(type: .target, reference: "TestmacOS", platformFilter: .macOS) + let dependency4 = Dependency(type: .package(product: "Swinject"), reference: "Swinject", platformFilter: .iOS) + let target = Target(name: "Test", type: .application, platform: .iOS, sources: ["Sources"], dependencies: [dependency1, dependency2, dependency3, dependency4]) + let swinjectPackage = SwiftPackage.remote(url: "https://github.com/Swinject/Swinject", versionRequirement: .exact("2.8.0")) + let project = Project(basePath: directoryPath, name: "Test", targets: [target, target1, target2, target3], packages: ["Swinject": swinjectPackage]) + + let pbxProj = try project.generatePbxProj() + + let targets = pbxProj.projects.first?.targets + let testTarget = pbxProj.projects.first?.targets.first(where: { $0.name == "Test" }) + let testTargetDependencies = testTarget?.dependencies + try expect(targets?.count) == 4 + try expect(testTargetDependencies?.count) == 3 + try expect(testTargetDependencies?[0].platformFilter).beNil() + try expect(testTargetDependencies?[1].platformFilter) == "ios" + try expect(testTargetDependencies?[2].platformFilter) == "maccatalyst" + try expect(testTarget?.frameworksBuildPhase()?.files?.count) == 1 + try expect(testTarget?.frameworksBuildPhase()?.files?[0].platformFilter) == "ios" + } + + $0.it("places resources before sources buildPhase") { + let directories = """ + Sources: + - MainScreen: + - Entities: + - file.swift + - image.jpg + """ + try createDirectories(directories) + let target1 = Target( + name: "TestAll", + type: .application, + platform: .iOS, + sources: ["Sources"], + putResourcesBeforeSourcesBuildPhase: true + ) + let target2 = Target( + name: "TestiOS", + type: .application, + platform: .iOS, + sources: ["Sources"], + putResourcesBeforeSourcesBuildPhase: false + ) + + let project = Project(basePath: directoryPath, name: "Test", targets: [target1, target2]) + + let pbxProj = try project.generatePbxProj() + + let targets = pbxProj.projects.first?.targets + try expect(targets?.count) == 2 + try expect(targets?.first?.buildPhases.first).to.beOfType(PBXResourcesBuildPhase.self) + try expect(targets?.first?.buildPhases.last).to.beOfType(PBXSourcesBuildPhase.self) + + try expect(targets?.last?.buildPhases.first).to.beOfType(PBXSourcesBuildPhase.self) + try expect(targets?.last?.buildPhases.last).to.beOfType(PBXResourcesBuildPhase.self) + } + } + } } diff --git a/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift b/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift index 07f63a925..8c9c43f90 100644 --- a/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SchemeGeneratorTests.swift @@ -479,6 +479,33 @@ class SchemeGeneratorTests: XCTestCase { try expect(xcscheme.testAction?.macroExpansion?.buildableName) == "MyApp.app" try expect(xcscheme.launchAction?.macroExpansion?.buildableName) == "MyApp.app" } + + $0.it("generates scheme with test target of local swift package") { + let targetScheme = TargetScheme( + testTargets: [Scheme.Test.TestTarget(targetReference: TestableTargetReference(name: "XcodeGenKitTests", location: .package("XcodeGen")))]) + let app = Target( + name: "MyApp", + type: .application, + platform: .iOS, + dependencies: [ + Dependency(type: .package(product: nil), reference: "XcodeGen") + ], + scheme: targetScheme + ) + let project = Project( + name: "ios_test", + targets: [app], + packages: ["XcodeGen": .local(path: "../", group: nil)] + ) + let xcodeProject = try project.generateXcodeProject() + let xcscheme = try unwrap(xcodeProject.sharedData?.schemes.first) + let buildableReference = try unwrap(xcscheme.testAction?.testables.first?.buildableReference) + + try expect(buildableReference.blueprintIdentifier) == "XcodeGenKitTests" + try expect(buildableReference.blueprintName) == "XcodeGenKitTests" + try expect(buildableReference.buildableName) == "XcodeGenKitTests" + try expect(buildableReference.referencedContainer) == "container:../" + } $0.it("allows to override test macroExpansion") { let app = Target(