-
-
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
Conversation
Sources/xcproj/PBXProj+Helpers.swift
Outdated
|
||
/// Returns project's root group | ||
public var rootGroup: PBXGroup { | ||
let groupRef = objects.projects[rootObject]!.mainGroup |
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'd make this property optional and remove the force unwraps from this getter
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 was considering that of course but it will lead to always force unwrapping or checking for nil when using this API, which will make it inconvenient. Is it possible that a project can have no rootObject or group with such reference? If so probably it's a malformed project, so failing should be expected in this case or this should be validated even earlier. As a user I would prefer convenience over safety in this particular case.
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 also don't like forced unwapping. It can be legitimately be missing when you are constructing project file from scratch. In that case it would be bad behaviour to crash in runtime. Instead this should be either optional variable or a function that throws an error.
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 would create helper for accessing rootProject
.
var rootProject: PBXProject? {
return objects.projects[rootObject]
}
This would make usage of rootObject
different. Instead you would need to access it through project: .rootProject?.mainGroup
. But it would be more clear what is optional.
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 it possible that a project can have no rootObject or group with such reference?
There are two usages of xcproj that I can think of right now:
- Opening a project and modify it.
- Create a new project.
In the first case, it's very unlikely that we force unwrap a nil value since the project already has a root group. However, in the second case, it might happen. This is a bad usage sign because the developer should have created the root group before accessing it.
It depends on whether we want the library to crash at runtime, or execute with unexpected behavior. Thinking about a developer consuming the API, I'd prefer the crash rather than spending time debugging where the unexpected behavior might be coming from.
What do you think @allu22 ?
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.
Example where rootObject
id
is set and root project does not exist until generate
is called:
https://github.com/yonaskolb/XcodeGen/blob/46b1512c7c4dde0621c25e4388d5e02823d3f6ab/Sources/XcodeGenKit/PBXProjGenerator.swift#L33
I think there is no right or wrong approach here. The question is, how defensive you want to be in the API. Either way is fine by me. But in case of unwrapping it here, maybe call fatalError
with message that hints that you are missing a project object?
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.
How about this?
public var rootGroup: PBXGroup {
guard let rootProject = self.rootProject else {
fatalError("Missing root project")
}
guard let rootGroup = objects.groups[rootProject.mainGroup] else {
fatalError("Root project has no root group")
}
return rootGroup
}
/// | ||
/// - 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
sourcesBuildPhases
is deprecated. Instead you should use objects.sourcesBuildPhases
.
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
👍 sorry, missed that.
/// - options: additional options, default is empty set. | ||
/// - Returns: new group and its reference. | ||
public func addGroup( | ||
named groupName: String, |
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.
The problem that I see exposing this method with all these arguments is that if any property is introduced in the PBXGroup
class, we need to update this method to support it. Given that xcproj represents the objects with mutable classes what do you think about simplifying it:
public func addGroup(named groupName: String,
to toGroup: PBXGroup,
withFolder: Bool = false) -> [(String, PBXGroup)]
Notice that the method returns an array with all the groups that got created. If the developer passes a path like this /group_a/group_b/group_c
then it'd return [GroupA, GroupB, GroupC]
corresponding the last element in the array to the last path component. If the developer is interested in updating let's say, the usesTabs
property of all the created groups, it can be easily done iterating with a forEach
on the returned array.
On the other side, I think the implementation could be simplified adding some recursion:
// Given the path /component_a/component_b/component_c
1. Generate/Get the first component group: GroupA
2. Concatenate the group with the result of calling the method with GroupA and name /component_b/component_c: [GroupA, result[0], result[1], ...]
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.
Right, I didn't notice that those types are mutable classes, so it will makes sense to remove all those parameters. And I like your idea about returning array.
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.
As Xcode allows you to use /
in group name, i think this should not be used as separator.
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.
Can you show an example? At least Xcode 9 does not allow to rename groups using /
.
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.
Once you point a group to a folder with name different than group name, you can change group name, without changing the folder name.
Example: https://imgur.com/oveZwsI
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.
Ok. This seems to be allowed also when group is created without folder. What is the alternative then? We can require passing in array of group names instead. But then if /
is used in one of them we should not create a group with such name if it is created with folder, as it will probably lead to invalid project. We should probably normalise each item. But if it is created without folder we should allow that.
/// | ||
/// - Parameter target: target obejct | ||
/// - Parameter reference: file reference | ||
public func addBuildFile(toTarget target: PBXTarget, reference: String) { |
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.
What do you think about returning the PBXBuildFile
here?
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'll return file and reference to match other APIs
xcLanguageSpecificationIdentifier: String? = nil | ||
) -> (String, PBXFileReference) { | ||
|
||
if let existingFileReference = fileReferences.first(where: { $0.value.path == filePath.string }) { |
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 this method does a lot of assumptions. Let's say we have the following file structure:
group_a/
group_b/
file_a
Where file_a is represented as a PBXFileReference
with name file_a
and sourceTree .group
. file_a
is a child of the group b.
Now you call this method saying that you want to add a file with path /group_a/group_b/file_a
which already exists in our project. We won't get any existing reference when we actually should (because there's already a reference for that file). Moreover, if the path is relative the group, the group where reference gets added to as a child matters (and the developer might not know about it).
I'd aim for an implementation similar to the one above for groups. You'd call a method like this:
func addFile(named fileName: String, to toGroup: PBXGroup)
And it'd recursively generate the groups and file references and return them.
What do you think?
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.
This falls into the problem of calculating file paths, indeed path property can be relative in this case and so will not match the absolute path in the predicate. Maybe there is another way to do such check that I'm missing?
Regarding your suggestion to create groups and files together in this method I was doing that originally, but decided to break it into separate methods not to violate single responsibility. And it will hide details of project structure from developer that you mentioned in the issue discussion saving only one call for the user. So I would keep it as it is, just remove additional parameters as you suggested for another method.
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 the safest way would be to compare the absolute paths but that's a bit expensive because you'd need to generate the absolute path for all the file references that you want to compare. We could use the method that you showed in another PR for getting the absolute path and optimize it. That way your first check would be legitimate.
Regarding your point on creating files and groups I agree with you so forget what I said. I wish we could leverage the type system to prevent the developer from passing a file name such as path/to/my/file.swift
failing at compilation time. What do you think about throwing in case the string doesn't represent a file @ilyapuchka ?
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 was thinking about using this method too, but yeah, it may be too expensive.
Regarding name we can make a check check if path is file path with path.isFile
and then throw if it is not.
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.
Good job @ilyapuchka. I just left a few comments regarding some API methods that I'm not very sure about. Thanks for tackling this since it's been in the backlog for quite some time.
I wonder what other @xcodeswift/contributors think about it.
Sources/xcproj/PBXProj+Helpers.swift
Outdated
|
||
/// Returns project's root group | ||
public var rootGroup: PBXGroup { | ||
let groupRef = objects.projects[rootObject]!.mainGroup |
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 also don't like forced unwapping. It can be legitimately be missing when you are constructing project file from scratch. In that case it would be bad behaviour to crash in runtime. Instead this should be either optional variable or a function that throws an error.
/// | ||
/// - 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 comment
The reason will be displayed to describe this comment to others. Learn more.
sourcesBuildPhases
is deprecated. Instead you should use objects.sourcesBuildPhases
.
/// - options: additional options, default is empty set. | ||
/// - Returns: new group and its reference. | ||
public func addGroup( | ||
named groupName: String, |
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.
As Xcode allows you to use /
in group name, i think this should not be used as separator.
2cdec26
to
574070e
Compare
574070e
to
d2ca466
Compare
Rebased and addressed all the comments, except related to using |
} | ||
} | ||
|
||
extension Path { |
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 should be made fileprivate
or moved to separate file.
|
||
/// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
These parameter descriptions do not match function.
/// 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 obejct |
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.
type on work obejct
return (reference, buildFile) | ||
} | ||
|
||
public func fullPath(fileElement: PBXFileElement, reference: String, sourceRoot: Path) -> Path? { |
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.
could you add few unit tests to this function. I am not sure what cases it has to cover and how. Especially with groups.
/// - sourceTree: file source tree, default is `.group`. | ||
/// - name: file name, by default gets file name from the path | ||
/// - Returns: new or existing file and its reference. | ||
public func addFile(at filePath: Path, sourceRoot: Path) throws -> (String, PBXFileReference) { |
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.
Please add unit test for this function. At the moment, I have a feeling that filePath == fullPath(fileElement: $0.value, reference: $0.key, sourceRoot: sourceRoot)
check will fail unless filePath
is initialized as absolute path. But it should not be a absolute path as PBXFileReference
is created with sourceTree: .group
@allu22 done |
} | ||
|
||
let fileReference = PBXFileReference( | ||
sourceTree: .absolute, |
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.
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.
My suggestion:
addFile
function will take filePath and optional PBXGroup where nil will be a fallback to mainGroup.
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.
@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.
Some of the source tree values are not supported in file path calculation (file will be added with nil path), but we can extend it later, it feels that use cases for these values should be rare.
/// - sourceRoot: path to project's source root. | ||
/// - Returns: new or existing file and its reference. | ||
public func addFile(at filePath: Path, toGroup: PBXGroup, sourceTree: PBXSourceTree = .group, sourceRoot: Path) throws -> (reference: String, file: PBXFileReference) { | ||
guard filePath.isFile else { |
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.
Why is it necessary to check if the file really exists?
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.
First to not make project buildable otherwise (it will not build if this file reference is then added to the build phase). Second to not allow paths to folders.
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.
Actually you can non existing add file to build phase. This is common case with generated files. But as this is a helper function, I believe it is ok to make these kinds of checks here.
/// Returns root project's root group. | ||
public var rootGroup: PBXGroup { | ||
guard let rootProject = self.rootProject else { | ||
fatalError("Missing root project") |
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!👌
@allu22 could you have another look and tell us if you think it's ready to be merged? |
@ilyapuchka it seems that linting is failing, that's why Travis-CI is returning an error. |
Great work @ilyapuchka 👏 |
🎉 |
Resolves #120, #40
Short description 📝
Added some helper methods required for adding build files
Solution 📦
With these methods adding a new build file requires just few calls:
If approved for merge I'll add some integration tests.
This change is