-
-
Notifications
You must be signed in to change notification settings - Fork 320
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
Helper methods required for adding build file to the target #213
Changes from 5 commits
12a5cd0
6e2b308
a7accb9
d2ca466
2613306
d5bc256
ce3cfda
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import Foundation | ||
import PathKit | ||
|
||
// MARK: - PBXProj.Objects Extension (Public) | ||
|
||
|
@@ -15,7 +16,182 @@ public extension PBXProj.Objects { | |
targets.append(contentsOf: Array(aggregateTargets.values) as [PBXTarget]) | ||
return targets.filter { $0.name == name } | ||
} | ||
|
||
|
||
/// Retruns target's sources build phase. | ||
/// | ||
/// - Parameter target: target object. | ||
/// - Returns: target's sources build phase, if found. | ||
public func sourcesBuildPhase(target: PBXTarget) -> PBXSourcesBuildPhase? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice additions @ilyapuchka π There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is exactly what is being used, this is extension on PBXObjects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π sorry, missed that. |
||
return sourcesBuildPhases.first(where: { target.buildPhases.contains($0.key) })?.value | ||
} | ||
|
||
/// Returns all files in target's sources build phase. | ||
/// | ||
/// - Parameter target: target object. | ||
/// - Returns: all files in target's sources build phase, or empty array if sources build phase is not found. | ||
public func sourceFiles(target: PBXTarget) -> [PBXFileElement] { | ||
return sourcesBuildPhase(target: target)?.files | ||
.flatMap({ buildFiles[$0]?.fileRef }) | ||
.flatMap(getFileElement(reference:)) ?? [] | ||
} | ||
|
||
/// Returns group with the given name contained in the given parent group and its reference. | ||
/// | ||
/// - Parameter groupName: group name. | ||
/// - Parameter inGroup: parent group. | ||
/// - Returns: group with the given name contained in the given parent group and its reference. | ||
public func group(named groupName: String, inGroup: PBXGroup) -> (String, PBXGroup)? { | ||
let children = inGroup.children | ||
return groups.first(where: { | ||
children.contains($0.key) && ($0.value.name == groupName || $0.value.path == groupName) | ||
}) | ||
} | ||
|
||
/// Adds new group with the give name to the given parent group. | ||
/// Group name can be a path with components separated by `/`. | ||
/// Will create new groups for intermediate path components or use existing groups. | ||
/// Returns all new or existing groups in the path and their references. | ||
/// | ||
/// - Parameters: | ||
/// - groupName: group name, can be a path with components separated by `/` | ||
/// - toGroup: parent group | ||
/// - options: additional options, default is empty set. | ||
/// - Returns: all new or existing groups in the path and their references. | ||
public func addGroup(named groupName: String, to toGroup: PBXGroup, options: GroupAddingOptions = []) -> [(reference: String, group: PBXGroup)] { | ||
return addGroups(groupName.components(separatedBy: "/"), to: toGroup, options: options) | ||
} | ||
|
||
private func addGroups(_ groupNames: [String], to toGroup: PBXGroup, options: GroupAddingOptions) -> [(reference: String, group: PBXGroup)] { | ||
guard !groupNames.isEmpty else { return [] } | ||
let newGroup = createOrGetGroup(named: groupNames[0], in: toGroup, options: options) | ||
return [newGroup] + addGroups(Array(groupNames.dropFirst()), to: newGroup.1, options: options) | ||
} | ||
|
||
private func createOrGetGroup(named groupName: String, in parentGroup: PBXGroup, options: GroupAddingOptions) -> (reference: String, group: PBXGroup) { | ||
if let existingGroup = group(named: groupName, inGroup: parentGroup) { | ||
return existingGroup | ||
} | ||
|
||
let newGroup = PBXGroup( | ||
children: [], | ||
sourceTree: .group, | ||
name: groupName, | ||
path: options.contains(.withoutFolder) ? nil : groupName | ||
) | ||
let reference = generateReference(newGroup, groupName) | ||
addObject(newGroup, reference: reference) | ||
parentGroup.children.append(reference) | ||
return (reference, newGroup) | ||
} | ||
|
||
/// Adds file at the give path to the project or returns existing file and its reference. | ||
/// | ||
/// - Parameters: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These parameter descriptions do not match function. |
||
/// - filePath: path to the file. | ||
/// - sourceRoot: path to project's source root. | ||
/// - Returns: new or existing file and its reference. | ||
public func addFile(at filePath: Path, sourceRoot: Path) throws -> (reference: String, file: PBXFileReference) { | ||
guard filePath.isFile else { throw XCodeProjEditingError.notAFile(path: filePath) } | ||
|
||
if let existingFileReference = fileReferences.first(where: { | ||
filePath == fullPath(fileElement: $0.value, reference: $0.key, sourceRoot: sourceRoot) | ||
}) { | ||
return (existingFileReference.key, existingFileReference.value) | ||
} | ||
|
||
let fileReference = PBXFileReference( | ||
sourceTree: .absolute, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding absolute path is very restrictive. This will restrict the usage of this project to single path in a filesystem. It would be useful only for situations where project is always generated. I recommend rethinking this helper function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @allu22 indeed! I improved this method by adding group and source tree parameters. Source tree is required to calculate initial value for file path property, as it will be different for different source tree values, i.e. it can be relative to group or to project. Also added helper method to calculate relative paths. I made it public as this method can be used for example to update file path when moving it to a different group. |
||
name: filePath.lastComponent, | ||
explicitFileType: PBXFileReference.fileType(path: filePath), | ||
lastKnownFileType: PBXFileReference.fileType(path: filePath), | ||
path: filePath.string | ||
) | ||
let reference = generateReference(fileReference, filePath.string) | ||
addObject(fileReference, reference: reference) | ||
return (reference, fileReference) | ||
} | ||
|
||
/// Adds file to the given group. | ||
/// If group already contains file with given reference method does nothing. | ||
/// | ||
/// - Parameter group: group to add file to | ||
/// - Parameter reference: file reference | ||
public func addFile(toGroup group: PBXGroup, reference: String) { | ||
guard !group.children.contains(reference) else { return } | ||
group.children.append(reference) | ||
} | ||
|
||
/// Adds file to the given target's sources build phase or returns existing build file and its reference. | ||
/// If target's sources build phase can't be found returns nil. | ||
/// | ||
/// - Parameter target: target object | ||
/// - Parameter reference: file reference | ||
/// - Returns: new or existing build file and its reference | ||
public func addBuildFile(toTarget target: PBXTarget, reference: String) -> (reference: String, file: PBXBuildFile)? { | ||
guard let sourcesBuildPhase = sourcesBuildPhase(target: target) else { return nil } | ||
if let existingBuildFile = buildFiles.first(where: { $0.value.fileRef == reference }) { | ||
return (existingBuildFile.key, existingBuildFile.value) | ||
} | ||
|
||
let buildFile = PBXBuildFile(fileRef: reference) | ||
let reference = generateReference(buildFile, reference) | ||
addObject(buildFile, reference: reference) | ||
sourcesBuildPhase.files.append(reference) | ||
return (reference, buildFile) | ||
} | ||
|
||
/// Returns full path of the file element. | ||
/// | ||
/// - Parameters: | ||
/// - fileElement: a file element | ||
/// - reference: a reference to this file element | ||
/// - sourceRoot: path to the project's sourceRoot | ||
/// - Returns: fully qualified file element path | ||
public func fullPath(fileElement: PBXFileElement, reference: String, sourceRoot: Path) -> Path? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you add few unit tests to this function. I am not sure what cases it has to cover and how. Especially with groups. |
||
switch fileElement.sourceTree { | ||
case .absolute?: | ||
return fileElement.path.flatMap({ Path($0) }) | ||
case .sourceRoot?: | ||
return fileElement.path.flatMap({ Path($0, relativeTo: sourceRoot) }) | ||
case .group?: | ||
guard let group = groups.first(where: { $0.value.children.contains(reference) }) else { return sourceRoot } | ||
guard let groupPath = fullPath(fileElement: group.value, reference: group.key, sourceRoot: sourceRoot) else { return nil } | ||
return fileElement.path.flatMap({ Path($0, relativeTo: groupPath) }) | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
} | ||
|
||
public struct GroupAddingOptions: OptionSet { | ||
public let rawValue: Int | ||
public init(rawValue: Int) { | ||
self.rawValue = rawValue | ||
} | ||
/// Create group without reference to folder | ||
static let withoutFolder = GroupAddingOptions(rawValue: 1 << 0) | ||
} | ||
|
||
public enum XCodeProjEditingError: Error, CustomStringConvertible { | ||
case notAFile(path: Path) | ||
|
||
public var description: String { | ||
switch self { | ||
case .notAFile(let path): | ||
return "\(path) is not a file path" | ||
} | ||
} | ||
} | ||
|
||
fileprivate extension Path { | ||
init(_ string: String, relativeTo relativePath: Path) { | ||
var path = Path(string) | ||
if !path.isAbsolute { | ||
path = (relativePath + path).absolute() | ||
} | ||
self.init(path.string) | ||
} | ||
} | ||
|
||
// MARK: - PBXProj.Objects Extension (Internal) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
fatalError
suitable here?Maybe we can make
rootGroup
optional (public var rootGroup: PBXGroup?
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's more suitable than requiring to unwrap this property which will be nil only in specific situations when accessed before project is correctly setup. In my opinion it would be better if API would not allow to create objects in such invalid or incomplete states (what it does now as far as I understand), but not sure what it will take to change it in this direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @ilyapuchka here. If we use an optional attribute, the developer could wrongly access that attribute and get unexpected behaviours as a result. If we
fatalErrror
they would know that they are doing something wrong and it'd save them some debugging time.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood!π