Skip to content

Commit

Permalink
Add Support for Custom Build Rules
Browse files Browse the repository at this point in the history
Some Xcode projects have custom build rules to automatically invoke a tool when an input file changes. Add a PBXObject subclass, PBXBuildRule, which describes when and how a custom build tool should be invoked. Update PBXProj to store custom build rules and add decoding/encoding support.

In addition to updating the tests, verified xcproj correctly round trips a project with custom build rules (custom pattern/custom tool, custom pattern/built-in tool, built-in pattern/custom tool, built-in pattern/built-in tool).
  • Loading branch information
briantkelley committed Dec 20, 2017
1 parent 6167d59 commit bb1e2ed
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
134 changes: 134 additions & 0 deletions Sources/xcproj/PBXBuildRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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?

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

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

/// 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,
fileType: String,
isEditable: UInt = 1,
name: String?,
outputFiles: [String],
outputFilesCompilerFlags: [String]?,
script: String) {
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
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)"))
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)) })
}
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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: 1,
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, 1)
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: 1,
name: "rule",
outputFiles:["a", "b"],
outputFilesCompilerFlags: ["-1", "-2"],
script: "script")
XCTAssertEqual(subject, another)
}
}
1 change: 1 addition & 0 deletions Tests/xcprojTests/PBXProjSpec.swift
Original file line number Diff line number Diff line change
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

0 comments on commit bb1e2ed

Please sign in to comment.