Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Support for Custom Build Rules #197

Merged
merged 1 commit into from Dec 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@
- Add breakpoint `condition` parameter by [@alexruperez](https://github.com/alexruperez).
- Support Xcode Extension product type https://github.com/xcodeswift/xcproj/pull/190 by @briantkelley
- Support for the legacy Build Carbon Resources build phase https://github.com/xcodeswift/xcproj/pull/196 by @briantkelley
- Support for custom build rules by https://github.com/xcodeswift/xcproj/pull/197 @briantkelley

## 1.7.0

Expand Down
6 changes: 6 additions & 0 deletions Carthage.xcodeproj/project.pbxproj
Expand Up @@ -60,6 +60,8 @@
BF4805463201 /* PathKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FR8873340702 /* PathKit.framework */; };
BF5094034701 /* PBXProj+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7428998701 /* PBXProj+Helpers.swift */; };
BF5094034702 /* PBXProj+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7428998701 /* PBXProj+Helpers.swift */; };
BF5139737101 /* PBXBuildRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7054588301 /* PBXBuildRule.swift */; };
BF5139737102 /* PBXBuildRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7054588301 /* PBXBuildRule.swift */; };
BF5156404201 /* String+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7128364401 /* String+Extras.swift */; };
BF5156404202 /* String+Extras.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR7128364401 /* String+Extras.swift */; };
BF5295135101 /* PBXShellScriptBuildPhase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FR2187351101 /* PBXShellScriptBuildPhase.swift */; };
Expand Down Expand Up @@ -160,6 +162,7 @@
FR6754770501 /* XCVersionGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCVersionGroup.swift; sourceTree = "<group>"; };
FR6980748501 /* PBXBuildFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBXBuildFile.swift; sourceTree = "<group>"; };
FR7045651001 /* PBXSourcesBuildPhase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBXSourcesBuildPhase.swift; sourceTree = "<group>"; };
FR7054588301 /* PBXBuildRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBXBuildRule.swift; sourceTree = "<group>"; };
FR7128364401 /* String+Extras.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extras.swift"; sourceTree = "<group>"; };
FR7414687801 /* PBXTargetDependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBXTargetDependency.swift; sourceTree = "<group>"; };
FR7428998701 /* PBXProj+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PBXProj+Helpers.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -255,6 +258,7 @@
FR3952162601 /* PBXAggregateTarget.swift */,
FR6980748501 /* PBXBuildFile.swift */,
FR4128075701 /* PBXBuildPhase.swift */,
FR7054588301 /* PBXBuildRule.swift */,
FR8962043601 /* PBXContainerItemProxy.swift */,
FR5460675201 /* PBXCopyFilesBuildPhase.swift */,
FR1364894901 /* PBXFileElement.swift */,
Expand Down Expand Up @@ -429,6 +433,7 @@
BF3873193301 /* PBXAggregateTarget.swift in Sources */,
BF4432284801 /* PBXBuildFile.swift in Sources */,
BF6638647201 /* PBXBuildPhase.swift in Sources */,
BF5139737101 /* PBXBuildRule.swift in Sources */,
BF7832125601 /* PBXContainerItemProxy.swift in Sources */,
BF7696587101 /* PBXCopyFilesBuildPhase.swift in Sources */,
BF3882936801 /* PBXFileElement.swift in Sources */,
Expand Down Expand Up @@ -488,6 +493,7 @@
BF3873193302 /* PBXAggregateTarget.swift in Sources */,
BF4432284802 /* PBXBuildFile.swift in Sources */,
BF6638647202 /* PBXBuildPhase.swift in Sources */,
BF5139737102 /* PBXBuildRule.swift in Sources */,
BF7832125602 /* PBXContainerItemProxy.swift in Sources */,
BF7696587102 /* PBXCopyFilesBuildPhase.swift in Sources */,
BF3882936802 /* PBXFileElement.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Fixtures/iOS/Project.xcodeproj/project.pbxproj
Expand Up @@ -20,6 +20,20 @@
23766C2B1EAA3484007A9026 /* iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23766C2A1EAA3484007A9026 /* iOSTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXBuildRule section */
6B7542351FE9CEDE003DFC29 /* PBXBuildRule */ = {
isa = PBXBuildRule;
compilerSpec = com.apple.compilers.proxy.script;
filePatterns = "*.myrule";
fileType = pattern.proxy;
isEditable = 1;
outputFiles = (
"$(DERIVED_FILE_DIR)/CompiledRule",
);
script = $TOOL_PATH/transform;
};
/* End PBXBuildRule section */

/* Begin PBXContainerItemProxy section */
23766C271EAA3484007A9026 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
Expand Down Expand Up @@ -157,6 +171,7 @@
23BB67531EE326A800BE9E79 /* Headers */,
);
buildRules = (
6B7542351FE9CEDE003DFC29 /* PBXBuildRule */,
);
dependencies = (
);
Expand Down
136 changes: 136 additions & 0 deletions Sources/xcproj/PBXBuildRule.swift
@@ -0,0 +1,136 @@
import Foundation

/// A PBXBuildRule is used to specify a method for transforming an input file in to an output file(s).
final public class PBXBuildRule: PBXObject, Equatable {

// MARK: - Attributes

/// Element compiler spec.
public var compilerSpec: String

/// Element file patterns.
public var filePatterns: String?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity. Did you check the optionality of the attributes with Xcode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this up and merging. @pepibumur, yes, I tried examining the encoded form of of a number of different permutations. compilerSpec is required so specify how the file should be processed, but filePatterns is optional since it's a filter on fileType.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem @briantkelley πŸ˜› it was just curiosity. The changes should be in xcproj 1.8.0 ready to be used :)


/// Element file type.
public var fileType: String

/// Element is editable.
public var isEditable: Bool

/// Element name.
public var name: String?

/// Element output files.
public var outputFiles: [String]

/// Element output files compiler flags.
public var outputFilesCompilerFlags: [String]?

/// Element script.
public var script: String?

// MARK: - Init

public init(reference: String,
compilerSpec: String,
filePatterns: String? = nil,
fileType: String,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more very minor thing. As fileType is non optional (I don't know if it should be or not), can we move it before optional filePatterns?

isEditable: Bool = true,
name: String? = nil,
outputFiles: [String] = [],
outputFilesCompilerFlags: [String]? = nil,
script: String? = nil) {
self.compilerSpec = compilerSpec
self.filePatterns = filePatterns
self.fileType = fileType
self.isEditable = isEditable
self.name = name
self.outputFiles = outputFiles
self.outputFilesCompilerFlags = outputFilesCompilerFlags
self.script = script
super.init(reference: reference)
}

// MARK: - Decodable

enum CodingKeys: String, CodingKey {
case compilerSpec
case filePatterns
case fileType
case isEditable
case name
case outputFiles
case outputFilesCompilerFlags
case script
}

public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.compilerSpec = try container.decodeIfPresent(.compilerSpec) ?? ""
self.filePatterns = try container.decodeIfPresent(.filePatterns)
self.fileType = try container.decodeIfPresent(.fileType) ?? ""
let isEditable: String? = try container.decodeIfPresent(.isEditable)
self.isEditable = (isEditable.flatMap(UInt.init) ?? 0) == 1
self.name = try container.decodeIfPresent(.name)
self.outputFiles = try container.decodeIfPresent(.outputFiles) ?? []
self.outputFilesCompilerFlags = try container.decodeIfPresent(.outputFilesCompilerFlags)
self.script = try container.decodeIfPresent(.script)
try super.init(from: decoder)
}

// MARK: - Equatable

public static func == (lhs: PBXBuildRule,
rhs: PBXBuildRule) -> Bool {
let outputFilesCompilerFlagsAreEqual: Bool = {
switch (lhs.outputFilesCompilerFlags, rhs.outputFilesCompilerFlags) {
case (.none, .none):
return true
case (.none, .some), (.some, .none):
return false
case (.some(let lhsOutputFilesCompilerFlags), .some(let rhsOutputFilesCompilerFlags)):
return lhsOutputFilesCompilerFlags == rhsOutputFilesCompilerFlags
}
}()
return lhs.reference == rhs.reference &&
lhs.compilerSpec == rhs.compilerSpec &&
lhs.filePatterns == rhs.filePatterns &&
lhs.fileType == rhs.fileType &&
lhs.isEditable == rhs.isEditable &&
lhs.name == rhs.name &&
lhs.outputFiles == rhs.outputFiles &&
outputFilesCompilerFlagsAreEqual &&
lhs.script == rhs.script
}
}

// MARK: - PBXBuildRule Extension (PlistSerializable)

extension PBXBuildRule: PlistSerializable {

var multiline: Bool { return true }

func plistKeyAndValue(proj: PBXProj) -> (key: CommentedString, value: PlistValue) {
var dictionary: [CommentedString: PlistValue] = [:]
dictionary["isa"] = .string(CommentedString(PBXBuildRule.isa))
dictionary["compilerSpec"] = .string(CommentedString(compilerSpec))
if let filePatterns = filePatterns {
dictionary["filePatterns"] = .string(CommentedString(filePatterns))
}
dictionary["fileType"] = .string(CommentedString(fileType))
dictionary["isEditable"] = .string(CommentedString("\(isEditable ? 1 : 0)"))
if let name = name {
dictionary["name"] = .string(CommentedString(name))
}
dictionary["outputFiles"] = .array(outputFiles.map { PlistValue.string(CommentedString($0)) })
if let outputFilesCompilerFlags = outputFilesCompilerFlags {
dictionary["outputFilesCompilerFlags"] = .array(outputFilesCompilerFlags.map { PlistValue.string(CommentedString($0)) })
}
if let script = script {
dictionary["script"] = .string(CommentedString(script))
}
return (key: CommentedString(self.reference, comment: PBXBuildRule.isa),
value: .dictionary(dictionary))
}

}
2 changes: 2 additions & 0 deletions Sources/xcproj/PBXObject.swift
Expand Up @@ -80,6 +80,8 @@ public class PBXObject: Referenceable, Decodable {
return try decoder.decode(XCVersionGroup.self, from: data)
case PBXRezBuildPhase.isa:
return try decoder.decode(PBXRezBuildPhase.self, from: data)
case PBXBuildRule.isa:
return try decoder.decode(PBXBuildRule.self, from: data)
default:
throw PBXObjectError.unknownElement(isa)
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/xcproj/PBXProj.swift
Expand Up @@ -19,6 +19,7 @@ final public class PBXProj: Decodable {
public var fileReferences: ReferenceableCollection<PBXFileReference> = [:]
public var projects: ReferenceableCollection<PBXProject> = [:]
public var referenceProxies: ReferenceableCollection<PBXReferenceProxy> = [:]
public var buildRules: ReferenceableCollection<PBXBuildRule> = [:]

// Build Phases
public var copyFilesBuildPhases: ReferenceableCollection<PBXCopyFilesBuildPhase> = [:]
Expand Down Expand Up @@ -71,7 +72,8 @@ final public class PBXProj: Decodable {
lhs.projects == rhs.projects &&
lhs.versionGroups == rhs.versionGroups &&
lhs.referenceProxies == rhs.referenceProxies &&
lhs.carbonResourcesBuildPhases == rhs.carbonResourcesBuildPhases
lhs.carbonResourcesBuildPhases == rhs.carbonResourcesBuildPhases &&
lhs.buildRules == rhs.buildRules
}

// MARK: - Public Methods
Expand Down Expand Up @@ -100,6 +102,7 @@ final public class PBXProj: Decodable {
case let object as XCVersionGroup: versionGroups.append(object)
case let object as PBXReferenceProxy: referenceProxies.append(object)
case let object as PBXRezBuildPhase: carbonResourcesBuildPhases.append(object)
case let object as PBXBuildRule: buildRules.append(object)
default: fatalError("Unhandled PBXObject type for \(object), this is likely a bug / todo")
}
}
Expand Down Expand Up @@ -164,6 +167,8 @@ final public class PBXProj: Decodable {
return object
} else if let object = carbonResourcesBuildPhases[reference] {
return object
} else if let object = buildRules[reference] {
return object
} else {
return nil
}
Expand Down
1 change: 1 addition & 0 deletions Sources/xcproj/PBXProjEncoder.swift
Expand Up @@ -30,6 +30,7 @@ final class PBXProjEncoder {
writeNewLine()
write(section: "PBXAggregateTarget", proj: proj, object: proj.objects.aggregateTargets)
write(section: "PBXBuildFile", proj: proj, object: proj.objects.buildFiles)
write(section: "PBXBuildRule", proj: proj, object: proj.objects.buildRules)
write(section: "PBXContainerItemProxy", proj: proj, object: proj.objects.containerItemProxies)
write(section: "PBXCopyFilesBuildPhase", proj: proj, object: proj.objects.copyFilesBuildPhases)
write(section: "PBXFileReference", proj: proj, object: proj.objects.fileReferences)
Expand Down
54 changes: 54 additions & 0 deletions Tests/xcprojTests/PBXBuildRuleSpec.swift
@@ -0,0 +1,54 @@
import Foundation
import XCTest
import xcproj

final class PBXBuildRuleSpec: XCTestCase {

var subject: PBXBuildRule!

override func setUp() {
super.setUp()
subject = PBXBuildRule(reference: "ref",
compilerSpec: "spec",
filePatterns: "pattern",
fileType: "type",
isEditable: true,
name: "rule",
outputFiles:["a", "b"],
outputFilesCompilerFlags: ["-1", "-2"],
script: "script")
}

func test_init_initializesTheBuildRuleWithTheRightAttributes() {
XCTAssertEqual(subject.reference, "ref")
XCTAssertEqual(subject.compilerSpec, "spec")
XCTAssertEqual(subject.filePatterns, "pattern")
XCTAssertEqual(subject.fileType, "type")
XCTAssertEqual(subject.isEditable, true)
XCTAssertEqual(subject.name, "rule")
XCTAssertEqual(subject.outputFiles, ["a", "b"])
XCTAssertEqual(subject.outputFilesCompilerFlags ?? [], ["-1", "-2"])
XCTAssertEqual(subject.script, "script")
}

func test_isa_returnsTheCorrectValue() {
XCTAssertEqual(PBXBuildRule.isa, "PBXBuildRule")
}

func test_hashValue_returnsTheReferenceHashValue() {
XCTAssertEqual(subject.hashValue, subject.reference.hashValue)
}

func test_equal_shouldReturnTheCorrectValue() {
let another = PBXBuildRule(reference: "ref",
compilerSpec: "spec",
filePatterns: "pattern",
fileType: "type",
isEditable: true,
name: "rule",
outputFiles:["a", "b"],
outputFilesCompilerFlags: ["-1", "-2"],
script: "script")
XCTAssertEqual(subject, another)
}
}
1 change: 1 addition & 0 deletions Tests/xcprojTests/PBXProjSpec.swift
Expand Up @@ -106,6 +106,7 @@ final class PBXProjIntegrationSpec: XCTestCase {
XCTAssertEqual(proj.objects.headersBuildPhases.count, 1)
XCTAssertEqual(proj.objects.nativeTargets.count, 2)
XCTAssertEqual(proj.objects.fileReferences.count, 15)
XCTAssertEqual(proj.objects.buildRules.count, 1)
XCTAssertEqual(proj.objects.versionGroups.count, 1)
XCTAssertEqual(proj.objects.projects.count, 1)
}
Expand Down