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

Use Custom File Name for .xcodeproj at Model Level #462

Merged
merged 19 commits into from Aug 7, 2019

Conversation

adamkhazi
Copy link
Contributor

@adamkhazi adamkhazi commented Jul 26, 2019

Resolves #461

Short description πŸ“

Add the ability to specify a custom file name at the model level to write an .xcodeproj during generation. Currently tuist uses the name of the project when saving the .xcodeproj to disk.

Why is this needed?

#461 goes into more detail but essentially this is needed to check two .xcodeprojs for equality more easily.

Solution πŸ“¦

Expose a fileName property in the project model (TuistGenerator/Models/Project.swift). By default it will use the name of the project if a fileName is not specified. Otherwise, it will use the fileName specified to write to disk i.e., filename.xcodeproj.

Implementation πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

  • Add fileName to Models/Project.swift which falls back to name if no fileName is specified
  • Make Generator/ProjectGenerator use project.fileName for the xcodeprojPath
  • Add Generator/ProjectGeneratorTests.swift test to check the filesystem for correctly written .xcodeproj name
  • Add Models/ProjectTests.swift test to check the model

@tuistbot
Copy link
Contributor

tuistbot commented Jul 26, 2019

1 Warning
⚠️ Have you introduced any user-facing changes? If so, please take some time to update the documentation. Keeping the documentation up to date makes it easier for users to learn how to use Tuist.

SwiftLint found issues

Warnings

File Line Reason
TuistConfig.swift 31 Line should be 150 characters or less: currently 156 characters (line_length)
ProjectGenerator.swift 83 Function body should span 40 lines or less excluding comments and whitespace: currently spans 41 lines (function_body_length)

Generated by 🚫 Danger

@codecov
Copy link

codecov bot commented Jul 26, 2019

Codecov Report

Merging #462 into master will increase coverage by 0.08%.
The diff coverage is 96.13%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master    #462      +/-   ##
=========================================
+ Coverage   92.32%   92.4%   +0.08%     
=========================================
  Files         334     336       +2     
  Lines       17373   17564     +191     
=========================================
+ Hits        16039   16230     +191     
  Misses       1334    1334
Impacted Files Coverage Ξ”
...rces/TuistKit/Generator/GeneratorModelLoader.swift 90.04% <100%> (+1.63%) ⬆️
Sources/TuistGenerator/Models/Project.swift 81.13% <100%> (+0.36%) ⬆️
...nerator/TestData/ProjectDescription+TestData.swift 100% <100%> (ΓΈ) ⬆️
...neratorTests/Generator/ProjectGeneratorTests.swift 99.28% <100%> (+0.14%) ⬆️
...KitTests/Generator/GeneratorModelLoaderTests.swift 97.8% <100%> (+0.18%) ⬆️
...es/TuistGenerator/Generator/ProjectGenerator.swift 96.17% <100%> (+0.02%) ⬆️
Sources/TuistGenerator/Generator/Generator.swift 100% <100%> (ΓΈ) ⬆️
...eratorTests/Models/TestData/Project+TestData.swift 100% <100%> (ΓΈ) ⬆️
...sts/ProjectDescriptionTests/TuistConfigTests.swift 100% <100%> (ΓΈ)
...ests/TuistGeneratorTests/Models/ProjectTests.swift 100% <100%> (ΓΈ) ⬆️
... and 6 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Ξ” = absolute <relative> (impact), ΓΈ = not affected, ? = missing data
Powered by Codecov. Last update 2285432...2f8282d. Read the comment docs.

Copy link
Contributor

@pepicrft pepicrft left a comment

Choose a reason for hiding this comment

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

Thanks for adding support for this @adamkhazi.

There's one thing though missing, which I think is important when adding features: exposing an API to the users. In the past, we added features like supporting multiple configurations, that users could not use even the logic for it was in the TuistGenerator module.

Together with it we should document the attribute, how it can be used, and maybe an use case for its usage.

Don't forget to update the CHANGELOG 😬

@adamkhazi
Copy link
Contributor Author

Happy to expose an API to users. πŸ˜€

I am worried about exposing it through the manifest description directly though as in the following example:

Project(name: "MyProject",
             fileName: "SomeFileName",
             targets: ...,
             schemes: ...,
             additionalFiles: ...
           )

I believe it may be confusing and may not fit the use case I described in #461. The distinction between the name and fileName in the manifest may be unclear as one will control the UUID generation (an implementation detail of tuist/xcodeproj) and the other will control what appears on disk (somename.xcodeproj) and in the Xcode UI.

Additionally, to result in two .xcodeprojs in the same folder (existing and new) from the same manifest users would have to change the original fileName in the manifest before running tuist generate to get an .xcodeproj adjacent to the current one. Make the .xcodeproj comparison. Then they would have to change the fileName back to the original if working in the context of a git repository.

Instead I suggest we add a tuist generate --file-suffix "-somesuffix" option. Then to generate an adjacent project to the current one all they would use is the one command. --file-suffix could use fileName in the project model internally to append to the project's current name. Project name A (from the manifest) with --file-suffix -somesuffix would become A-somesuffix.xcodeproj on disk.

Again, this is quite a niche use case and I am wondering whether this will be a widely applicable feature.

@pepicrft
Copy link
Contributor

I believe it may be confusing and may not fit the use case I described in #461.

Looking at the API you are right, it's confusing for the users. But I think it's also confusing in the internal framework, which ends up exposing an API that Tuist doesn't use. Someone looking at that filename attribute may wonder:

  • Why is this here if Tuist is not using it?
  • What is it for?

The other solution that you propose, --file-suffix is a big ambiguous. It's hard to say that the argument implies suffixing the generated projects. Considering that we allow globally configuring Tuist using a TuistConfig manifest, what about something along the lines of:

let config = TuistConfig(generationOptions: [
  .suffixProjectNames(with: "-yourSuffix")
])

What do you think @tuist/core ?

By the way, I'm not saying no to the feature 😬. I'm trying to fit it nicely in the public API to not end up with an internal API that just fits one user's need.

@ollieatkinson
Copy link
Collaborator

ollieatkinson commented Jul 26, 2019

Hey @adamkhazi!

The idea is certainly interesting - I can understand why it might be useful to provide some transforms over the defaults Tuist provides.

I am worried about exposing it through the manifest description

I also worry about this. Typically we try to make the manifest obvious to the end-user.

I like the idea @pepibumur has put forward - would that fit your use-case? or do you need to be able to do it on a per-project basis?

In the future I would also like us to be able to provide prefixes to other kinds of files. One I would like to see would be the manifest files themselves (e.g. Project.ci.swift). We should consider when implementing this change.

One change I would make would be to introduce the API like this so that we do not limit it to just prefixes.

let config = TuistConfig(generationOptions: [
  .xcodeProjectName("${TARGET_NAME}-yourSuffix")
])

I'm happy to discuss this more if we think it needs it.

@adamkhazi
Copy link
Contributor Author

adamkhazi commented Jul 29, 2019

Hey @ollieatkinson!

Putting the suffix inside the Tuist config as @pepibumur suggested would fit my use case well.

@ollieatkinson did you mean ${PROJECT_NAME} in your example? I am not sure which target name we would use by default for the file name as a project could obviously have many targets.

Extending @pepibumur's example I was thinking something like the following might be more obvious to users in the config instead of having a ${PROJECT_NAME} that I am worried might not be very discoverable:

let config = TuistConfig(generationOptions: [
   .suffixProjectName(with: String)
   // .prefixProjectName(with: String)
   // .suffixAndPrefixProjectName(with: String, and: String)
])
   

@ollieatkinson
Copy link
Collaborator

@ollieatkinson did you mean ${PROJECT_NAME} in your example? I am not sure which target name we would use by default for the file name as a project could obviously have many targets.

Yes, sorry.

The previous comment was just an example of intent rather than actual implementation. I would probably like to use the new string interpolation from Swift 5. e.g.

.xcodeProjectName("\(.projectName)-yourSuffix")

@adamkhazi
Copy link
Contributor Author

Sure, that makes sense. πŸ™‚

I will experiment with your interpolation example locally before posting some code.

@adamkhazi
Copy link
Contributor Author

What about something like the following?

struct TemplateString {
    let rawString: String
}

extension TemplateString: ExpressibleByStringLiteral {
    init(stringLiteral: String) {
        self.rawString = stringLiteral
    }
}

extension TemplateString: CustomStringConvertible {
    var description: String {
        return rawString
    }
}

extension TemplateString: ExpressibleByStringInterpolation {
    init(stringInterpolation: StringInterpolation) {
        self.rawString = stringInterpolation.string
    }
    
    struct StringInterpolation: StringInterpolationProtocol {
        var string: String
        
        init(literalCapacity: Int, interpolationCount: Int) {
            self.string = String()
        }
        
        mutating func appendLiteral(_ literal: String) {
            self.string.append(literal)
        }
    }
}

extension TemplateString {
    enum Token: String {
        case projectName = "${project_name}"
    }
}

extension TemplateString.StringInterpolation {
    mutating func appendInterpolation(_ token: TemplateString.Token) {
        self.string.append(token.rawValue)
    }
}

let projectName: TemplateString = "prefix-\(.projectName)-suffix"

@ollieatkinson
Copy link
Collaborator

API looks good, if you manage to push it up I can take a look at testing it out in a few of the fixtures.

@adamkhazi adamkhazi requested a review from pepicrft July 31, 2019 10:05

private func enriched(model: TuistGenerator.Project,
with config: TuistGenerator.TuistConfig) throws -> TuistGenerator.Project {
var enrichedModel = model
Copy link
Contributor

Choose a reason for hiding this comment

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

Notice that the model being passed is a reference, so creating another internal variable does not make any copy. I'd just remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do the following operations in the same method:

enrichedModel = enrichedModel.adding(target: manifestTarget)
...
enrichedModel = enrichedModel.replacing(fileName: xcodeFileName)

The idea was to be consistent and continue returning a new model every time a Project property is changed. Project's properties are immutable.

The alternative would be to change the lets inside Project to vars.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's keep it like that for now. If we see maintaining those methods that create copies of the instances become cumbersome, we can reconsider the approach.

@@ -242,12 +276,24 @@ extension TuistGenerator.Project {
func adding(target: TuistGenerator.Target) -> TuistGenerator.Project {
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that TuistGenerator.Project is a class, we don't need to be creating copies of the objects. Wherever adding and replacing is used, I'd just set the attribute to the object and not create new instances of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this is related to my previous comment about Project's properties being immutable.

features/generate.feature Outdated Show resolved Hide resolved
Copy link
Contributor

@pepicrft pepicrft left a comment

Choose a reason for hiding this comment

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

I added some minor nitpick comments. Excellent job here @adamkhazi.
I didn't know the template interpolation thing was possible in Swift. The API that you achieved in this PR is very beautiful ❀️

@adamkhazi adamkhazi requested a review from pepicrft August 6, 2019 14:57
@adamkhazi
Copy link
Contributor Author

Thanks @pepibumur! I believe the nitpicks should be addressed now apart from one. πŸ™‚

@pepicrft pepicrft merged commit 1aed58f into tuist:master Aug 7, 2019
@pepicrft
Copy link
Contributor

pepicrft commented Aug 7, 2019

Great job @adamkhazi

giphy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Specify Custom File Names for .xcodeproj During Generation
4 participants