From 20a99e50185e859d289eee156d78caeb20d98fd3 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Tue, 4 Aug 2020 09:14:32 +1000 Subject: [PATCH] Add FileTypes for cross project file options (#914) * move BuildPhase * add fileTypes * update changelog --- CHANGELOG.md | 3 +- Docs/ProjectSpec.md | 10 ++ Sources/ProjectSpec/BuildPhaseSpec.swift | 148 +++++++++++++++++ Sources/ProjectSpec/FileType.swift | 114 +++++++++++++ Sources/ProjectSpec/SourceType.swift | 14 ++ Sources/ProjectSpec/SpecOptions.swift | 9 + Sources/ProjectSpec/TargetSource.swift | 154 +----------------- Sources/XcodeGenKit/PBXProjGenerator.swift | 8 +- Sources/XcodeGenKit/SourceGenerator.swift | 107 ++++++------ .../Fixtures/TestProject/App_iOS/Resource.abc | 0 .../App_iOS/Resource.abcd/File.json | 0 .../Project.xcodeproj/project.pbxproj | 4 + Tests/Fixtures/TestProject/project.yml | 5 + Tests/ProjectSpecTests/SpecLoadingTests.swift | 13 ++ .../SourceGeneratorTests.swift | 81 +++++++-- 15 files changed, 448 insertions(+), 222 deletions(-) create mode 100644 Sources/ProjectSpec/BuildPhaseSpec.swift create mode 100644 Sources/ProjectSpec/FileType.swift create mode 100644 Sources/ProjectSpec/SourceType.swift create mode 100644 Tests/Fixtures/TestProject/App_iOS/Resource.abc create mode 100644 Tests/Fixtures/TestProject/App_iOS/Resource.abcd/File.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a06745..1e8c3961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Next Version #### Added -- Add `onlyCopyFilesOnInstall` option to targets for the Embed Files build phase. [#912](https://github.com/yonaskolb/XcodeGen/pull/912) @jsorge +- Added `options.fileTypes` which lets you set cross project defaults for certain file extensions [#914](https://github.com/yonaskolb/XcodeGen/pull/914) @yonaskolb +- Added `onlyCopyFilesOnInstall` option to targets for the Embed Files build phase. [#912](https://github.com/yonaskolb/XcodeGen/pull/912) @jsorge #### Fixed - Treat all directories with known UTI as file wrapper. [#896](https://github.com/yonaskolb/XcodeGen/pull/896) @KhaosT diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index cfbe0987..9853d5cd 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -123,6 +123,7 @@ Note that target names can also be changed by adding a `name` property to a targ - [ ] **generateEmptyDirectories**: **Bool** - If this is `true` then empty directories will be added to project too else will be missed. Defaults to `false`. - [ ] **findCarthageFrameworks**: **Bool** - When this is set to `true`, all the invididual frameworks for Carthage dependencies will automatically be found. This property can be overriden individually for each carthage dependency - for more details see See **findFrameworks** in the [Dependency](#dependency) section. Defaults to `false`. - [ ] **localPackagesGroup**: **String** - The group name that local packages are put into. This defaults to `Packages` +- [ ] **fileTypes**: **[String: [FileType](#filetype)]** - A list of default file options for specific file extensions across the project. Values in [Sources](#sources) will overwrite these settings. - [ ] **preGenCommand**: **String** - A bash command to run before the project has been generated. If the project isn't generated due to no changes when using the cache then this won't run. This is useful for running things like generating resources files before the project is regenerated. - [ ] **postGenCommand**: **String** - A bash command to run after the project has been generated. If the project isn't generated due to no changes when using the cache then this won't run. This is useful for running things like `pod install` only if the project is actually regenerated. @@ -151,6 +152,15 @@ options: In this example, we set up the order of two groups. First one is the main group, i.e. the project, note that in this case, we shouldn't set `pattern` option and the second group order is for groups whose names ends with `Screen`. +### FileType +Default settings for file extensions. See [Sources](#sources) for more documentation on properties. If you overwrite an extension that XcodeGen already provides by default, you will need to provide all the settings. + +- [ ] **file**: **Bool** - Whether this extension should be treated like a file. Defaults to true. +- [ ] **buildPhase**: **String** - The default build phase. +- [ ] **attributes**: **[String]** - Additional settings attributes that will be applied to any build files. +- [ ] **resourceTags**: **[String]** - On Demand Resource Tags that will be applied to any resources. This also adds to the project attribute's knownAssetTags. +- [ ] **compilerFlags**: **[String]** - A list of compiler flags to add. + ### Configs Each config maps to a build type of either `debug` or `release` which will then apply default build settings to the project. Any value other than `debug` or `release` (for example `none`), will mean no default build settings will be applied to the project. diff --git a/Sources/ProjectSpec/BuildPhaseSpec.swift b/Sources/ProjectSpec/BuildPhaseSpec.swift new file mode 100644 index 00000000..716e1bec --- /dev/null +++ b/Sources/ProjectSpec/BuildPhaseSpec.swift @@ -0,0 +1,148 @@ +// +// File.swift +// +// +// Created by Yonas Kolb on 1/5/20. +// + +import Foundation +import XcodeProj +import JSONUtilities + +public enum BuildPhaseSpec: Equatable { + case sources + case headers + case resources + case copyFiles(CopyFilesSettings) + case none + // Not currently exposed as selectable options, but used internally + case frameworks + case runScript + case carbonResources + + public struct CopyFilesSettings: Equatable, Hashable { + public static let xpcServices = CopyFilesSettings( + destination: .productsDirectory, + subpath: "$(CONTENTS_FOLDER_PATH)/XPCServices", + phaseOrder: .postCompile + ) + + public enum Destination: String { + case absolutePath + case productsDirectory + case wrapper + case executables + case resources + case javaResources + case frameworks + case sharedFrameworks + case sharedSupport + case plugins + + public var destination: PBXCopyFilesBuildPhase.SubFolder? { + switch self { + case .absolutePath: return .absolutePath + case .productsDirectory: return .productsDirectory + case .wrapper: return .wrapper + case .executables: return .executables + case .resources: return .resources + case .javaResources: return .javaResources + case .frameworks: return .frameworks + case .sharedFrameworks: return .sharedFrameworks + case .sharedSupport: return .sharedSupport + case .plugins: return .plugins + } + } + } + + public enum PhaseOrder: String { + /// Run before the Compile Sources phase + case preCompile + /// Run after the Compile Sources and post-compile Run Script phases + case postCompile + } + + public var destination: Destination + public var subpath: String + public var phaseOrder: PhaseOrder + + public init( + destination: Destination, + subpath: String, + phaseOrder: PhaseOrder + ) { + self.destination = destination + self.subpath = subpath + self.phaseOrder = phaseOrder + } + } + + public var buildPhase: BuildPhase? { + switch self { + case .sources: return .sources + case .headers: return .headers + case .resources: return .resources + case .copyFiles: return .copyFiles + case .frameworks: return .frameworks + case .runScript: return .runScript + case .carbonResources: return .carbonResources + case .none: return nil + } + } +} + +extension BuildPhaseSpec { + + public init(string: String) throws { + switch string { + case "sources": self = .sources + case "headers": self = .headers + case "resources": self = .resources + case "copyFiles": + throw SpecParsingError.invalidSourceBuildPhase("copyFiles must specify a \"destination\" and optional \"subpath\"") + case "none": self = .none + default: + throw SpecParsingError.invalidSourceBuildPhase(string.quoted) + } + } +} + +extension BuildPhaseSpec: JSONObjectConvertible { + + public init(jsonDictionary: JSONDictionary) throws { + self = .copyFiles(try jsonDictionary.json(atKeyPath: "copyFiles")) + } +} + +extension BuildPhaseSpec: JSONEncodable { + public func toJSONValue() -> Any { + switch self { + case .sources: return "sources" + case .headers: return "headers" + case .resources: return "resources" + case .copyFiles(let files): return ["copyFiles": files.toJSONValue()] + case .none: return "none" + case .frameworks: fatalError("invalid build phase") + case .runScript: fatalError("invalid build phase") + case .carbonResources: fatalError("invalid build phase") + } + } +} + +extension BuildPhaseSpec.CopyFilesSettings: JSONObjectConvertible { + + public init(jsonDictionary: JSONDictionary) throws { + destination = try jsonDictionary.json(atKeyPath: "destination") + subpath = jsonDictionary.json(atKeyPath: "subpath") ?? "" + phaseOrder = .postCompile + } +} + +extension BuildPhaseSpec.CopyFilesSettings: JSONEncodable { + public func toJSONValue() -> Any { + [ + "destination": destination.rawValue, + "subpath": subpath, + ] + } +} diff --git a/Sources/ProjectSpec/FileType.swift b/Sources/ProjectSpec/FileType.swift new file mode 100644 index 00000000..bc535a1d --- /dev/null +++ b/Sources/ProjectSpec/FileType.swift @@ -0,0 +1,114 @@ +// +// File.swift +// +// +// Created by Yonas Kolb on 1/5/20. +// + +import Foundation +import JSONUtilities +import enum XcodeProj.BuildPhase + +public struct FileType: Equatable { + + public enum Defaults { + public static let file = true + } + + public var file: Bool + public var buildPhase: BuildPhaseSpec? + public var attributes: [String] + public var resourceTags: [String] + public var compilerFlags: [String] + + public init( + file: Bool = Defaults.file, + buildPhase: BuildPhaseSpec? = nil, + attributes: [String] = [], + resourceTags: [String] = [], + compilerFlags: [String] = [] + ) { + self.file = file + self.buildPhase = buildPhase + self.attributes = attributes + self.resourceTags = resourceTags + self.compilerFlags = compilerFlags + } +} + +extension FileType: JSONObjectConvertible { + public init(jsonDictionary: JSONDictionary) throws { + if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") { + buildPhase = try BuildPhaseSpec(string: string) + } else if let dict: JSONDictionary = jsonDictionary.json(atKeyPath: "buildPhase") { + buildPhase = try BuildPhaseSpec(jsonDictionary: dict) + } + file = jsonDictionary.json(atKeyPath: "file") ?? Defaults.file + attributes = jsonDictionary.json(atKeyPath: "attributes") ?? [] + resourceTags = jsonDictionary.json(atKeyPath: "resourceTags") ?? [] + compilerFlags = jsonDictionary.json(atKeyPath: "compilerFlags") ?? [] + } +} + +extension FileType: JSONEncodable { + public func toJSONValue() -> Any { + var dict: [String: Any?] = [ + "buildPhase": buildPhase?.toJSONValue(), + "attributes": attributes, + "resourceTags": resourceTags, + "compilerFlags": compilerFlags, + ] + if file != Defaults.file { + dict["file"] = file + } + return dict + } +} + +extension FileType { + + public static let defaultFileTypes: [String: FileType] = [ + // resources + "bundle": FileType(buildPhase: .resources), + "xcassets": FileType(buildPhase: .resources), + + // sources + "swift": FileType(buildPhase: .sources), + "m": FileType(buildPhase: .sources), + "mm": FileType(buildPhase: .sources), + "cpp": FileType(buildPhase: .sources), + "c": FileType(buildPhase: .sources), + "cc": FileType(buildPhase: .sources), + "S": FileType(buildPhase: .sources), + "xcdatamodeld": FileType(buildPhase: .sources), + "xcmappingmodel": FileType(buildPhase: .sources), + "intentdefinition": FileType(buildPhase: .sources), + "metal": FileType(buildPhase: .sources), + "mlmodel": FileType(buildPhase: .sources), + "rcproject": FileType(buildPhase: .sources), + + // headers + "h": FileType(buildPhase: .headers), + "hh": FileType(buildPhase: .headers), + "hpp": FileType(buildPhase: .headers), + "ipp": FileType(buildPhase: .headers), + "tpp": FileType(buildPhase: .headers), + "hxx": FileType(buildPhase: .headers), + "def": FileType(buildPhase: .headers), + + // frameworks + "framework": FileType(buildPhase: .frameworks), + + // copyfiles + "xpc": FileType(buildPhase: .copyFiles(.xpcServices)), + + // no build phase (not resources) + "xcconfig": FileType(buildPhase: BuildPhaseSpec.none), + "entitlements": FileType(buildPhase: BuildPhaseSpec.none), + "gpx": FileType(buildPhase: BuildPhaseSpec.none), + "lproj": FileType(buildPhase: BuildPhaseSpec.none), + "xcfilelist": FileType(buildPhase: BuildPhaseSpec.none), + "apns": FileType(buildPhase: BuildPhaseSpec.none), + "pch": FileType(buildPhase: BuildPhaseSpec.none), + ] +} diff --git a/Sources/ProjectSpec/SourceType.swift b/Sources/ProjectSpec/SourceType.swift new file mode 100644 index 00000000..77ce4ffe --- /dev/null +++ b/Sources/ProjectSpec/SourceType.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Yonas Kolb on 1/5/20. +// + +import Foundation + +public enum SourceType: String { + case group + case file + case folder +} diff --git a/Sources/ProjectSpec/SpecOptions.swift b/Sources/ProjectSpec/SpecOptions.swift index 237f2b56..b2d35a9d 100644 --- a/Sources/ProjectSpec/SpecOptions.swift +++ b/Sources/ProjectSpec/SpecOptions.swift @@ -27,6 +27,7 @@ public struct SpecOptions: Equatable { public var transitivelyLinkDependencies: Bool public var groupSortPosition: GroupSortPosition public var groupOrdering: [GroupOrdering] + public var fileTypes: [String: FileType] public var generateEmptyDirectories: Bool public var findCarthageFrameworks: Bool public var localPackagesGroup: String? @@ -87,6 +88,7 @@ public struct SpecOptions: Equatable { transitivelyLinkDependencies: Bool = transitivelyLinkDependenciesDefault, groupSortPosition: GroupSortPosition = groupSortPositionDefault, groupOrdering: [GroupOrdering] = [], + fileTypes: [String: FileType] = [:], generateEmptyDirectories: Bool = generateEmptyDirectoriesDefault, findCarthageFrameworks: Bool = findCarthageFrameworksDefault, localPackagesGroup: String? = nil, @@ -110,6 +112,7 @@ public struct SpecOptions: Equatable { self.transitivelyLinkDependencies = transitivelyLinkDependencies self.groupSortPosition = groupSortPosition self.groupOrdering = groupOrdering + self.fileTypes = fileTypes self.generateEmptyDirectories = generateEmptyDirectories self.findCarthageFrameworks = findCarthageFrameworks self.localPackagesGroup = localPackagesGroup @@ -146,6 +149,11 @@ extension SpecOptions: JSONObjectConvertible { localPackagesGroup = jsonDictionary.json(atKeyPath: "localPackagesGroup") preGenCommand = jsonDictionary.json(atKeyPath: "preGenCommand") postGenCommand = jsonDictionary.json(atKeyPath: "postGenCommand") + if jsonDictionary["fileTypes"] != nil { + fileTypes = try jsonDictionary.json(atKeyPath: "fileTypes") + } else { + fileTypes = [:] + } } } @@ -169,6 +177,7 @@ extension SpecOptions: JSONEncodable { "localPackagesGroup": localPackagesGroup, "preGenCommand": preGenCommand, "postGenCommand": postGenCommand, + "fileTypes": fileTypes ] if settingPresets != SpecOptions.settingPresetsDefault { diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index 8c55b787..2a75f3bf 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -1,8 +1,6 @@ import Foundation import JSONUtilities import PathKit -import enum XcodeProj.BuildPhase -import class XcodeProj.PBXCopyFilesBuildPhase public struct TargetSource: Equatable { public static let optionalDefault = false @@ -15,7 +13,7 @@ public struct TargetSource: Equatable { public var includes: [String] public var type: SourceType? public var optional: Bool - public var buildPhase: BuildPhase? + public var buildPhase: BuildPhaseSpec? public var headerVisibility: HeaderVisibility? public var createIntermediateGroups: Bool? public var attributes: [String] @@ -35,94 +33,6 @@ public struct TargetSource: Equatable { } } - public enum BuildPhase: Equatable { - case sources - case headers - case resources - case copyFiles(CopyFilesSettings) - case none - // Not currently exposed as selectable options, but used internally - case frameworks - case runScript - case carbonResources - - public struct CopyFilesSettings: Equatable, Hashable { - public static let xpcServices = CopyFilesSettings( - destination: .productsDirectory, - subpath: "$(CONTENTS_FOLDER_PATH)/XPCServices", - phaseOrder: .postCompile - ) - - public enum Destination: String { - case absolutePath - case productsDirectory - case wrapper - case executables - case resources - case javaResources - case frameworks - case sharedFrameworks - case sharedSupport - case plugins - - public var destination: PBXCopyFilesBuildPhase.SubFolder? { - switch self { - case .absolutePath: return .absolutePath - case .productsDirectory: return .productsDirectory - case .wrapper: return .wrapper - case .executables: return .executables - case .resources: return .resources - case .javaResources: return .javaResources - case .frameworks: return .frameworks - case .sharedFrameworks: return .sharedFrameworks - case .sharedSupport: return .sharedSupport - case .plugins: return .plugins - } - } - } - - public enum PhaseOrder: String { - /// Run before the Compile Sources phase - case preCompile - /// Run after the Compile Sources and post-compile Run Script phases - case postCompile - } - - public var destination: Destination - public var subpath: String - public var phaseOrder: PhaseOrder - - public init( - destination: Destination, - subpath: String, - phaseOrder: PhaseOrder - ) { - self.destination = destination - self.subpath = subpath - self.phaseOrder = phaseOrder - } - } - - public var buildPhase: XcodeProj.BuildPhase? { - switch self { - case .sources: return .sources - case .headers: return .headers - case .resources: return .resources - case .copyFiles: return .copyFiles - case .frameworks: return .frameworks - case .runScript: return .runScript - case .carbonResources: return .carbonResources - case .none: return nil - } - } - } - - public enum SourceType: String { - case group - case file - case folder - } - public init( path: String, name: String? = nil, @@ -132,7 +42,7 @@ public struct TargetSource: Equatable { includes: [String] = [], type: SourceType? = nil, optional: Bool = optionalDefault, - buildPhase: BuildPhase? = nil, + buildPhase: BuildPhaseSpec? = nil, headerVisibility: HeaderVisibility? = nil, createIntermediateGroups: Bool? = nil, attributes: [String] = [], @@ -188,9 +98,9 @@ extension TargetSource: JSONObjectConvertible { optional = jsonDictionary.json(atKeyPath: "optional") ?? TargetSource.optionalDefault if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") { - buildPhase = try BuildPhase(string: string) + buildPhase = try BuildPhaseSpec(string: string) } else if let dict: JSONDictionary = jsonDictionary.json(atKeyPath: "buildPhase") { - buildPhase = try BuildPhase(jsonDictionary: dict) + buildPhase = try BuildPhaseSpec(jsonDictionary: dict) } createIntermediateGroups = jsonDictionary.json(atKeyPath: "createIntermediateGroups") @@ -228,62 +138,6 @@ extension TargetSource: JSONEncodable { } } -extension TargetSource.BuildPhase { - - public init(string: String) throws { - switch string { - case "sources": self = .sources - case "headers": self = .headers - case "resources": self = .resources - case "copyFiles": - throw SpecParsingError.invalidSourceBuildPhase("copyFiles must specify a \"destination\" and optional \"subpath\"") - case "none": self = .none - default: - throw SpecParsingError.invalidSourceBuildPhase(string.quoted) - } - } -} - -extension TargetSource.BuildPhase: JSONObjectConvertible { - - public init(jsonDictionary: JSONDictionary) throws { - self = .copyFiles(try jsonDictionary.json(atKeyPath: "copyFiles")) - } -} - -extension TargetSource.BuildPhase: JSONEncodable { - public func toJSONValue() -> Any { - switch self { - case .sources: return "sources" - case .headers: return "headers" - case .resources: return "resources" - case .copyFiles(let files): return ["copyFiles": files.toJSONValue()] - case .none: return "none" - case .frameworks: fatalError("invalid build phase") - case .runScript: fatalError("invalid build phase") - case .carbonResources: fatalError("invalid build phase") - } - } -} - -extension TargetSource.BuildPhase.CopyFilesSettings: JSONObjectConvertible { - - public init(jsonDictionary: JSONDictionary) throws { - destination = try jsonDictionary.json(atKeyPath: "destination") - subpath = jsonDictionary.json(atKeyPath: "subpath") ?? "" - phaseOrder = .postCompile - } -} - -extension TargetSource.BuildPhase.CopyFilesSettings: JSONEncodable { - public func toJSONValue() -> Any { - [ - "destination": destination.rawValue, - "subpath": subpath, - ] - } -} - extension TargetSource: PathContainer { static var pathProperties: [PathProperty] { diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 8049cdc1..c0c61c74 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -476,7 +476,7 @@ public class PBXProjGenerator { return addObject(shellScriptPhase) } - func generateCopyFiles(targetName: String, copyFiles: TargetSource.BuildPhase.CopyFilesSettings, buildPhaseFiles: [PBXBuildFile]) -> PBXCopyFilesBuildPhase { + func generateCopyFiles(targetName: String, copyFiles: BuildPhaseSpec.CopyFilesSettings, buildPhaseFiles: [PBXBuildFile]) -> PBXCopyFilesBuildPhase { let copyFilesBuildPhase = PBXCopyFilesBuildPhase( dstPath: copyFiles.subpath, dstSubfolderSpec: copyFiles.destination.destination, @@ -644,7 +644,7 @@ public class PBXProjGenerator { var dependencies: [PBXTargetDependency] = [] var targetFrameworkBuildFiles: [PBXBuildFile] = [] var frameworkBuildPaths = Set() - var copyFilesBuildPhasesFiles: [TargetSource.BuildPhase.CopyFilesSettings: [PBXBuildFile]] = [:] + var copyFilesBuildPhasesFiles: [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] = [:] var copyFrameworksReferences: [PBXBuildFile] = [] var copyResourcesReferences: [PBXBuildFile] = [] var copyBundlesReferences: [PBXBuildFile] = [] @@ -955,8 +955,8 @@ public class PBXProjGenerator { return getBuildFilesForSourceFiles(filteredSourceFiles) } - func getBuildFilesForCopyFilesPhases() -> [TargetSource.BuildPhase.CopyFilesSettings: [PBXBuildFile]] { - var sourceFilesByCopyFiles: [TargetSource.BuildPhase.CopyFilesSettings: [SourceFile]] = [:] + func getBuildFilesForCopyFilesPhases() -> [BuildPhaseSpec.CopyFilesSettings: [PBXBuildFile]] { + var sourceFilesByCopyFiles: [BuildPhaseSpec.CopyFilesSettings: [SourceFile]] = [:] for sourceFile in sourceFiles { guard case let .copyFiles(copyFilesSettings)? = sourceFile.buildPhase else { continue } sourceFilesByCopyFiles[copyFilesSettings, default: []].append(sourceFile) diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 481ed3b9..17255a90 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -8,7 +8,7 @@ struct SourceFile { let path: Path let fileReference: PBXFileElement let buildFile: PBXBuildFile - let buildPhase: TargetSource.BuildPhase? + let buildPhase: BuildPhaseSpec? } class SourceGenerator { @@ -87,11 +87,22 @@ class SourceGenerator { _ = try getSourceFiles(targetType: .none, targetSource: TargetSource(path: path), path: fullPath) } - func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, buildPhase: TargetSource.BuildPhase? = nil, fileReference: PBXFileElement? = nil) -> SourceFile { + func getFileType(path: Path) -> FileType? { + if let fileExtension = path.extension { + return project.options.fileTypes[fileExtension] ?? FileType.defaultFileTypes[fileExtension] + } else { + return nil + } + } + + func generateSourceFile(targetType: PBXProductType, targetSource: TargetSource, path: Path, buildPhase: BuildPhaseSpec? = nil, fileReference: PBXFileElement? = nil) -> SourceFile { let fileReference = fileReference ?? fileReferencesByPath[path.string.lowercased()]! var settings: [String: Any] = [:] - var attributes: [String] = targetSource.attributes - var chosenBuildPhase: TargetSource.BuildPhase? + let fileType = getFileType(path: path) + var attributes: [String] = targetSource.attributes + (fileType?.attributes ?? []) + var chosenBuildPhase: BuildPhaseSpec? + var compilerFlags: String = "" + let assetTags: [String] = targetSource.resourceTags + (fileType?.resourceTags ?? []) let headerVisibility = targetSource.headerVisibility ?? .public @@ -107,7 +118,7 @@ class SourceGenerator { // Static libraries don't support the header build phase // For public headers they need to be copied if headerVisibility == .public { - chosenBuildPhase = .copyFiles(TargetSource.BuildPhase.CopyFilesSettings( + chosenBuildPhase = .copyFiles(BuildPhaseSpec.CopyFilesSettings( destination: .productsDirectory, subpath: "include/$(PRODUCT_NAME)", phaseOrder: .preCompile @@ -123,16 +134,28 @@ class SourceGenerator { attributes.append(headerVisibility.settingName) } } - if chosenBuildPhase == .sources && targetSource.compilerFlags.count > 0 { - settings["COMPILER_FLAGS"] = targetSource.compilerFlags.joined(separator: " ") + + if let flags = fileType?.compilerFlags { + compilerFlags += flags.joined(separator: " ") + } + + if !targetSource.compilerFlags.isEmpty { + if !compilerFlags.isEmpty { + compilerFlags += " " + } + compilerFlags += targetSource.compilerFlags.joined(separator: " ") + } + + if chosenBuildPhase == .sources && !compilerFlags.isEmpty { + settings["COMPILER_FLAGS"] = compilerFlags } if !attributes.isEmpty { settings["ATTRIBUTES"] = attributes } - - if chosenBuildPhase == .resources && !targetSource.resourceTags.isEmpty { - settings["ASSET_TAGS"] = targetSource.resourceTags + + if chosenBuildPhase == .resources && !assetTags.isEmpty { + settings["ASSET_TAGS"] = assetTags } let buildFile = PBXBuildFile(file: fileReference, settings: settings.isEmpty ? nil : settings) @@ -226,53 +249,22 @@ class SourceGenerator { } /// returns a default build phase for a given path. This is based off the filename - private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> TargetSource.BuildPhase? { + private func getDefaultBuildPhase(for path: Path, targetType: PBXProductType) -> BuildPhaseSpec? { if path.lastComponent == "Info.plist" { return nil } + if let buildPhase = getFileType(path: path)?.buildPhase { + return buildPhase + } if let fileExtension = path.extension { switch fileExtension { - case "swift", - "m", - "mm", - "cpp", - "c", - "cc", - "S", - "xcdatamodeld", - "xcmappingmodel", - "intentdefinition", - "metal", - "mlmodel", - "rcproject": - return .sources - case "h", - "hh", - "hpp", - "ipp", - "tpp", - "hxx", - "def": - return .headers case "modulemap": guard targetType == .staticLibrary else { return nil } - return .copyFiles(TargetSource.BuildPhase.CopyFilesSettings( + return .copyFiles(BuildPhaseSpec.CopyFilesSettings( destination: .productsDirectory, subpath: "include/$(PRODUCT_NAME)", phaseOrder: .preCompile )) - case "framework": - return .frameworks - case "xpc": - return .copyFiles(.xpcServices) - case "xcconfig", - "entitlements", - "gpx", - "lproj", - "xcfilelist", - "apns", - "pch": - return nil default: return .resources } @@ -414,13 +406,24 @@ class SourceGenerator { let children = try getSourceChildren(targetSource: targetSource, dirPath: path, excludePaths: excludePaths, includePaths: includePaths) let createIntermediateGroups = targetSource.createIntermediateGroups ?? project.options.createIntermediateGroups + let nonLocalizedChildren = children.filter { $0.extension != "lproj" } - let directories = children - .filter { $0.isDirectory && !Xcode.isDirectoryFileWrapper(path: $0) && $0.extension != "lproj" } + let directories = nonLocalizedChildren + .filter { + if let fileType = getFileType(path: $0) { + return !fileType.file + } else { + return $0.isDirectory && !Xcode.isDirectoryFileWrapper(path: $0) + } + } - let filePaths = children - .filter { $0.isFile || $0.isDirectory && $0.extension != "lproj" - && Xcode.isDirectoryFileWrapper(path: $0) + let filePaths = nonLocalizedChildren + .filter { + if let fileType = getFileType(path: $0) { + return fileType.file + } else { + return $0.isFile || $0.isDirectory && Xcode.isDirectoryFileWrapper(path: $0) + } } let localisedDirectories = children @@ -576,7 +579,7 @@ class SourceGenerator { rootGroups.insert(fileReference) } - let buildPhase: TargetSource.BuildPhase? + let buildPhase: BuildPhaseSpec? if let targetBuildPhase = targetSource.buildPhase { buildPhase = targetBuildPhase } else { diff --git a/Tests/Fixtures/TestProject/App_iOS/Resource.abc b/Tests/Fixtures/TestProject/App_iOS/Resource.abc new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Fixtures/TestProject/App_iOS/Resource.abcd/File.json b/Tests/Fixtures/TestProject/App_iOS/Resource.abcd/File.json new file mode 100644 index 00000000..e69de29b diff --git a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj index 7fe07ec4..96e72c62 100644 --- a/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj +++ b/Tests/Fixtures/TestProject/Project.xcodeproj/project.pbxproj @@ -467,6 +467,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 01E6934B571B91EAAFF0EDCB /* Resource.abc */ = {isa = PBXFileReference; path = Resource.abc; sourceTree = ""; }; 020E4DA91C9132845CAFDC5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 039F208D1138598CE060F140 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 03D6D1E34022DA9524E5B38D /* Mintfile */ = {isa = PBXFileReference; path = Mintfile; sourceTree = ""; }; @@ -495,6 +496,7 @@ 2A5F527F2590C14956518174 /* FrameworkFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameworkFile.swift; sourceTree = ""; }; 2E1E747C7BC434ADB80CC269 /* Headers */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Headers; sourceTree = SOURCE_ROOT; }; 2F430AABE04B7499B458D9DB /* SwiftFileInDotPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFileInDotPath.swift; sourceTree = ""; }; + 325F18855099386B08DD309B /* Resource.abcd */ = {isa = PBXFileReference; path = Resource.abcd; sourceTree = ""; }; 33F6DCDC37D2E66543D4965D /* App_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34F13B632328979093CE6056 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 3571E41E19A5AB8AAAB04109 /* StandaloneAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = StandaloneAssets.xcassets; sourceTree = ""; }; @@ -726,6 +728,8 @@ BF59AC868D227C92CA8B1B57 /* Model.xcmappingmodel */, C7809CE9FE9852C2AA87ACE5 /* module.modulemap */, 553D289724905857912C7A1D /* outputList.xcfilelist */, + 01E6934B571B91EAAFF0EDCB /* Resource.abc */, + 325F18855099386B08DD309B /* Resource.abcd */, 8AF20308873AEEEC4D8C45D1 /* Settings.bundle */, 0704B6CAFBB53E0EBB08F6B3 /* ViewController.swift */, ); diff --git a/Tests/Fixtures/TestProject/project.yml b/Tests/Fixtures/TestProject/project.yml index fe352a0c..793b3171 100644 --- a/Tests/Fixtures/TestProject/project.yml +++ b/Tests/Fixtures/TestProject/project.yml @@ -11,6 +11,11 @@ options: groupSortPosition: top preGenCommand: echo "This is a pre-gen command" postGenCommand: scripts/script.sh + fileTypes: + abc: + buildPhase: none + abcd: + buildPhase: none fileGroups: - Configs - FileGroup diff --git a/Tests/ProjectSpecTests/SpecLoadingTests.swift b/Tests/ProjectSpecTests/SpecLoadingTests.swift index 82e5a182..26635e76 100644 --- a/Tests/ProjectSpecTests/SpecLoadingTests.swift +++ b/Tests/ProjectSpecTests/SpecLoadingTests.swift @@ -1110,6 +1110,12 @@ class SpecLoadingTests: XCTestCase { watchOS: "3.0", macOS: "10.12.1" ), + fileTypes: ["abc": FileType( + file: false, + buildPhase: .sources, + attributes: ["a1", "a2"], + resourceTags: ["r1", "r2"], + compilerFlags: ["c1", "c2"])], findCarthageFrameworks: true, preGenCommand: "swiftgen", postGenCommand: "pod install" @@ -1125,6 +1131,13 @@ class SpecLoadingTests: XCTestCase { "findCarthageFrameworks": true, "preGenCommand": "swiftgen", "postGenCommand": "pod install", + "fileTypes": ["abc": [ + "file": false, + "buildPhase": "sources", + "attributes": ["a1", "a2"], + "resourceTags": ["r1", "r2"], + "compilerFlags": ["c1", "c2"], + ]] ]] let parsedSpec = try getProjectSpec(dictionary) try expect(parsedSpec) == expected diff --git a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift index 162ac17c..2a38a7f3 100644 --- a/Tests/XcodeGenKitTests/SourceGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/SourceGeneratorTests.swift @@ -602,7 +602,7 @@ class SourceGeneratorTests: XCTestCase { let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [ TargetSource(path: "A", buildPhase: .resources), - TargetSource(path: "B", buildPhase: TargetSource.BuildPhase.none), + TargetSource(path: "B", buildPhase: BuildPhaseSpec.none), TargetSource(path: "C", buildPhase: nil), ]) let project = Project(basePath: directoryPath, name: "Test", targets: [target]) @@ -614,11 +614,11 @@ class SourceGeneratorTests: XCTestCase { try pbxProj.expectFile(paths: ["A", "Info.plist"], buildPhase: .resources) try pbxProj.expectFile(paths: ["A", "file.xcconfig"], buildPhase: .resources) - try pbxProj.expectFile(paths: ["B", "file.swift"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["B", "file.xcassets"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["B", "file.h"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["B", "Info.plist"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["B", "file.xcconfig"], buildPhase: TargetSource.BuildPhase.none) + try pbxProj.expectFile(paths: ["B", "file.swift"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["B", "file.xcassets"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["B", "file.h"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["B", "Info.plist"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["B", "file.xcconfig"], buildPhase: BuildPhaseSpec.none) try pbxProj.expectFile(paths: ["C", "file.swift"], buildPhase: .sources) try pbxProj.expectFile(paths: ["C", "file.m"], buildPhase: .sources) @@ -633,16 +633,16 @@ class SourceGeneratorTests: XCTestCase { try pbxProj.expectFile(paths: ["C", "file.tpp"], buildPhase: .headers) try pbxProj.expectFile(paths: ["C", "file.hxx"], buildPhase: .headers) try pbxProj.expectFile(paths: ["C", "file.def"], buildPhase: .headers) - try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.entitlements"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.gpx"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.apns"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: TargetSource.BuildPhase.none) - try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: TargetSource.BuildPhase.none) + try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.entitlements"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.gpx"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.apns"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["C", "file.xcconfig"], buildPhase: BuildPhaseSpec.none) try pbxProj.expectFile(paths: ["C", "file.xcassets"], buildPhase: .resources) try pbxProj.expectFile(paths: ["C", "file.123"], buildPhase: .resources) - try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: TargetSource.BuildPhase.none) + try pbxProj.expectFile(paths: ["C", "Info.plist"], buildPhase: BuildPhaseSpec.none) try pbxProj.expectFile(paths: ["C", "file.metal"], buildPhase: .sources) try pbxProj.expectFile(paths: ["C", "file.mlmodel"], buildPhase: .sources) try pbxProj.expectFile(paths: ["C", "Intent.intentdefinition"], buildPhase: .sources) @@ -654,6 +654,57 @@ class SourceGeneratorTests: XCTestCase { try pbxProj.expectFile(paths: ["C", "WithPeriod2.0", "file.swift"], buildPhase: .sources) } + $0.it("sets file type properties") { + let directories = """ + A: + - file.resource1 + - file.source1 + - file.abc: + - file.a + - file.exclude1 + - file.unphased1 + - ignored.swift + """ + try createDirectories(directories) + + let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [ + TargetSource(path: "A"), + ]) + let project = Project(basePath: directoryPath, name: "Test", targets: [target], options: .init(fileTypes: [ + "abc": FileType(buildPhase: .sources), + "source1": FileType(buildPhase: .sources, attributes: ["a1", "a2"], resourceTags: ["r1", "r2"], compilerFlags: ["-c1", "-c2"]), + "resource1": FileType(buildPhase: .resources, attributes: ["a1", "a2"], resourceTags: ["r1", "r2"], compilerFlags: ["-c1", "-c2"]), + "unphased1": FileType(buildPhase: BuildPhaseSpec.none), + "swift": FileType(buildPhase: .resources), + ])) + + let pbxProj = try project.generatePbxProj() + try pbxProj.expectFile(paths: ["A", "file.abc"], buildPhase: .sources) + try pbxProj.expectFile(paths: ["A", "file.source1"], buildPhase: .sources) + try pbxProj.expectFile(paths: ["A", "file.resource1"], buildPhase: .resources) + try pbxProj.expectFile(paths: ["A", "file.unphased1"], buildPhase: BuildPhaseSpec.none) + try pbxProj.expectFile(paths: ["A", "ignored.swift"], buildPhase: .resources) + + do { + let fileReference = try unwrap(pbxProj.getFileReference(paths: ["A", "file.resource1"], names: ["A", "file.resource1"])) + let buildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file === fileReference })) + let settings = NSDictionary(dictionary: buildFile.settings ?? [:]) + try expect(settings) == [ + "ATTRIBUTES": ["a1", "a2"], + "ASSET_TAGS": ["r1", "r2"], + ] + } + do { + let fileReference = try unwrap(pbxProj.getFileReference(paths: ["A", "file.source1"], names: ["A", "file.source1"])) + let buildFile = try unwrap(pbxProj.buildFiles.first(where: { $0.file === fileReference })) + let settings = NSDictionary(dictionary: buildFile.settings ?? [:]) + try expect(settings) == [ + "ATTRIBUTES": ["a1", "a2"], + "COMPILER_FLAGS": "-c1 -c2", + ] + } + } + $0.it("duplicate TargetSource is included once in sources build phase") { let directories = """ Sources: @@ -1084,7 +1135,7 @@ class SourceGeneratorTests: XCTestCase { extension PBXProj { /// expect a file within groups of the paths, using optional different names - func expectFile(paths: [String], names: [String]? = nil, buildPhase: TargetSource.BuildPhase? = nil, file: String = #file, line: Int = #line) throws { + func expectFile(paths: [String], names: [String]? = nil, buildPhase: BuildPhaseSpec? = nil, file: String = #file, line: Int = #line) throws { guard let fileReference = getFileReference(paths: paths, names: names ?? paths) else { var error = "Could not find file at path \(paths.joined(separator: "/").quoted)" if let names = names, names != paths {