From cf6df044cfd5635203802cf20d80d7b65a17d6b9 Mon Sep 17 00:00:00 2001 From: Yonas Kolb Date: Fri, 22 Dec 2017 18:57:15 +0800 Subject: [PATCH] add TargetSource.buildPhase for overriding build phase --- Docs/ProjectSpec.md | 5 + Sources/ProjectSpec/SpecParsingError.swift | 2 + Sources/ProjectSpec/TargetSource.swift | 29 ++++- Sources/XcodeGenKit/SourceGenerator.swift | 14 ++- .../ProjectGeneratorTests.swift | 105 +++++++++++++++++- 5 files changed, 147 insertions(+), 8 deletions(-) diff --git a/Docs/ProjectSpec.md b/Docs/ProjectSpec.md index 0ae49678..11ea74b4 100644 --- a/Docs/ProjectSpec.md +++ b/Docs/ProjectSpec.md @@ -216,6 +216,11 @@ A source can be provided via a string (the path) or an object of the form: - [ ] **name**: **String** - Can be used to override the name of the source file or directory. By default the last component of the path is used for the name - [ ] **compilerFlags**: **[String]** or **String** - A list of compilerFlags to add to files under this specific path provided as a list or a space delimitted string. Defaults to empty. - [ ] **excludes**: **[String]** or **String** - A list of global patterns representing the files to exclude. +- [ ] **buildPhase**: **String** - This manually sets the build phase this file or files in this directory will be added to, otherwise XcodeGen will guess based on the file extension. Note that `Info.plist` files will never be added to any build phases, no matter what this setting is. Possible values are: + - `sources` - Compile Sources phase + - `resources` - Copy Bundle Resources phase + - `headers` - Headers Phase + - `none` - Will not be added to any build phases - [ ] **type**: **String**: This can be one of the following values - `file`: a file reference with a parent group will be created (Default for files or directories with extensions) - `group`: a group with all it's containing files. (Default for directories without extensions) diff --git a/Sources/ProjectSpec/SpecParsingError.swift b/Sources/ProjectSpec/SpecParsingError.swift index 57cf2cb0..5183eb5e 100644 --- a/Sources/ProjectSpec/SpecParsingError.swift +++ b/Sources/ProjectSpec/SpecParsingError.swift @@ -4,12 +4,14 @@ public enum SpecParsingError: Error, CustomStringConvertible { case unknownTargetType(String) case unknownTargetPlatform(String) case invalidDependency([String: Any]) + case unknownSourceBuildPhase(String) public var description: String { switch self { case let .unknownTargetType(type): return "Unknown Target type: \(type)" case let .unknownTargetPlatform(platform): return "Unknown Target platform: \(platform)" case let .invalidDependency(dependency): return "Unknown Target dependency: \(dependency)" + case let .unknownSourceBuildPhase(buildPhase): return "Unknown Source Build Phase: \(buildPhase)" } } } diff --git a/Sources/ProjectSpec/TargetSource.swift b/Sources/ProjectSpec/TargetSource.swift index f1e427a5..8c7b0c64 100644 --- a/Sources/ProjectSpec/TargetSource.swift +++ b/Sources/ProjectSpec/TargetSource.swift @@ -1,6 +1,7 @@ import Foundation import JSONUtilities import PathKit +import xcproj public struct TargetSource { @@ -10,6 +11,23 @@ public struct TargetSource { public var excludes: [String] public var type: SourceType? public var optional: Bool + public var buildPhase: BuildPhase? + + public enum BuildPhase: String { + case sources + case headers + case resources + case none + + public var buildPhase: xcproj.BuildPhase? { + switch self { + case .sources: return .sources + case .headers: return .headers + case .resources: return .resources + case .none: return nil + } + } + } public enum SourceType: String { case group @@ -17,13 +35,14 @@ public struct TargetSource { case folder } - public init(path: String, name: String? = nil, compilerFlags: [String] = [], excludes: [String] = [], type: SourceType? = nil, optional: Bool = false) { + public init(path: String, name: String? = nil, compilerFlags: [String] = [], excludes: [String] = [], type: SourceType? = nil, optional: Bool = false, buildPhase: BuildPhase? = nil) { self.path = path self.name = name self.compilerFlags = compilerFlags self.excludes = excludes self.type = type self.optional = optional + self.buildPhase = buildPhase } } @@ -56,6 +75,13 @@ extension TargetSource: JSONObjectConvertible { excludes = jsonDictionary.json(atKeyPath: "excludes") ?? [] type = jsonDictionary.json(atKeyPath: "type") optional = jsonDictionary.json(atKeyPath: "optional") ?? false + if let string: String = jsonDictionary.json(atKeyPath: "buildPhase") { + if let buildPhase = BuildPhase(rawValue: string) { + self.buildPhase = buildPhase + } else { + throw SpecParsingError.unknownSourceBuildPhase(string) + } + } } } @@ -68,5 +94,6 @@ extension TargetSource: Equatable { && lhs.excludes == rhs.excludes && lhs.type == rhs.type && lhs.optional == rhs.optional + && lhs.buildPhase == rhs.buildPhase } } diff --git a/Sources/XcodeGenKit/SourceGenerator.swift b/Sources/XcodeGenKit/SourceGenerator.swift index 95a337b0..9229fd53 100644 --- a/Sources/XcodeGenKit/SourceGenerator.swift +++ b/Sources/XcodeGenKit/SourceGenerator.swift @@ -44,9 +44,17 @@ class SourceGenerator { func generateSourceFile(targetSource: TargetSource, path: Path, buildPhase: BuildPhase? = nil) -> SourceFile { let fileReference = fileReferencesByPath[path]! var settings: [String: Any] = [:] - let buildPhase = buildPhase ?? getDefaultBuildPhase(for: path) + let chosenBuildPhase: BuildPhase? - if buildPhase == .headers { + if let buildPhase = buildPhase { + chosenBuildPhase = buildPhase + } else if let buildPhase = targetSource.buildPhase { + chosenBuildPhase = buildPhase.buildPhase + } else { + chosenBuildPhase = getDefaultBuildPhase(for: path) + } + + if chosenBuildPhase == .headers { settings = ["ATTRIBUTES": ["Public"]] } if targetSource.compilerFlags.count > 0 { @@ -55,7 +63,7 @@ class SourceGenerator { // TODO: add the target name to the reference generator string so shared files don't have same reference (that will be escaped by appending a number) let buildFile = PBXBuildFile(reference: referenceGenerator.generate(PBXBuildFile.self, fileReference + targetName), fileRef: fileReference, settings: settings.isEmpty ? nil : settings) - return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile, buildPhase: buildPhase) + return SourceFile(path: path, fileReference: fileReference, buildFile: buildFile, buildPhase: chosenBuildPhase) } func getContainedFileReference(path: Path) -> String { diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index 428255b9..32d9c436 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -571,6 +571,89 @@ func projectGeneratorTests() { try project.expectFile(paths: ["Sources/A"], names: ["A"], buildPhase: .resources) try project.expectFileMissing(paths: ["Sources", "A", "a.swift"]) } + + $0.it("adds files to correct build phase") { + let directories = """ + A: + - file.swift + - file.xcassets + - file.h + - Info.plist + - file.xcconfig + B: + - file.swift + - file.xcassets + - file.h + - Info.plist + - file.xcconfig + C: + - file.swift + - file.m + - file.mm + - file.cpp + - file.c + - file.S + - file.h + - file.hh + - file.hpp + - file.ipp + - file.tpp + - file.hxx + - file.def + - file.xcconfig + - file.entitlements + - file.gpx + - file.apns + - file.123 + - file.xcassets + - Info.plist + """ + try createDirectories(directories) + + let target = Target(name: "Test", type: .framework, platform: .iOS, sources: [ + TargetSource(path: "A", buildPhase: .resources), + TargetSource(path: "B", buildPhase: .none), + TargetSource(path: "C", buildPhase: nil), + ]) + let spec = ProjectSpec(basePath: directoryPath, name: "Test", targets: [target]) + + let project = try getPbxProj(spec) + try project.expectFile(paths: ["A", "file.swift"], buildPhase: .resources) + try project.expectFile(paths: ["A", "file.xcassets"], buildPhase: .resources) + try project.expectFile(paths: ["A", "file.h"], buildPhase: .resources) + try project.expectFile(paths: ["A", "Info.plist"], buildPhase: .none) + try project.expectFile(paths: ["A", "file.xcconfig"], buildPhase: .resources) + + try project.expectFile(paths: ["B", "file.swift"], buildPhase: .none) + try project.expectFile(paths: ["B", "file.xcassets"], buildPhase: .none) + try project.expectFile(paths: ["B", "file.h"], buildPhase: .none) + try project.expectFile(paths: ["B", "Info.plist"], buildPhase: .none) + try project.expectFile(paths: ["B", "file.xcconfig"], buildPhase: .none) + + try project.expectFile(paths: ["C", "file.swift"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.m"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.mm"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.cpp"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.c"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.S"], buildPhase: .sources) + try project.expectFile(paths: ["C", "file.h"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.hh"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.hpp"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.ipp"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.tpp"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.hxx"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.def"], buildPhase: .headers) + try project.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.entitlements"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.gpx"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.apns"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.xcconfig"], buildPhase: .none) + try project.expectFile(paths: ["C", "file.xcassets"], buildPhase: .resources) + try project.expectFile(paths: ["C", "file.123"], buildPhase: .resources) + try project.expectFile(paths: ["C", "Info.plist"], buildPhase: .none) + } } } } @@ -600,7 +683,7 @@ extension PBXProj { extension PBXProj { /// expect a file within groups of the paths, using optional different names - func expectFile(paths: [String], names: [String]? = nil, buildPhase: BuildPhase? = nil) throws { + func expectFile(paths: [String], names: [String]? = nil, buildPhase: TargetSource.BuildPhase? = nil) 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 { @@ -610,9 +693,23 @@ extension PBXProj { } if let buildPhase = buildPhase { - guard let buildFile = objects.buildFiles.referenceValues.first(where: { $0.fileRef == fileReference.reference }), - objects.buildPhases.referenceValues.contains(where: { $0.files.contains(buildFile.reference) }) else { - throw failure("File \(paths.joined(separator: "/").quoted) is not in a \(buildPhase.rawValue.quoted) build phase") + let buildFile = objects.buildFiles.referenceValues.first(where: { $0.fileRef == fileReference.reference }) + let actualBuildPhase = buildFile.flatMap { buildFile in objects.buildPhases.referenceValues.first { $0.files.contains(buildFile.reference) } }?.buildPhase + + var error: String? + if let buildPhase = buildPhase.buildPhase { + if actualBuildPhase != buildPhase { + if let actualBuildPhase = actualBuildPhase { + error = "is in the \(actualBuildPhase.rawValue) build phase instead of the expected \(buildPhase.rawValue.quoted)" + } else { + error = "isn't in a build phase when it's expected to be in \(buildPhase.rawValue.quoted)" + } + } + } else if let actualBuildPhase = actualBuildPhase { + error = "is in the \(actualBuildPhase.rawValue.quoted) build phase when it's expected to not be in any" + } + if let error = error { + throw failure("File \(paths.joined(separator: "/").quoted) \(error)") } } }