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
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- Allow specifying multiple configurations within project manifests https://github.com/tuist/tuist/pull/451 by @kwridan
- Add linting for mismatching build configurations in a workspace https://github.com/tuist/tuist/pull/474 by @kwridan
- Support for CocoaPods dependencies https://github.com/tuist/tuist/pull/465 by @pepibumur
- Support custom .xcodeproj name at the model level https://github.com/tuist/tuist/pull/462 by @adamkhazi

### Fixed

Expand Down
56 changes: 56 additions & 0 deletions Sources/ProjectDescription/TemplateString.swift
@@ -0,0 +1,56 @@
import Foundation

public struct TemplateString: Encodable, Decodable, Equatable {
/// Contains a string that can be interpolated with options.
let rawString: String
}

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

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

extension TemplateString: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
rawString = stringInterpolation.string
}

public struct StringInterpolation: StringInterpolationProtocol {
var string: String

public init(literalCapacity _: Int, interpolationCount _: Int) {
string = String()
}

public mutating func appendLiteral(_ literal: String) {
string.append(literal)
}

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

extension TemplateString {
/// Provides a template for existing project properties.
///
/// - projectName: The name of the project.
public enum Token: String {
case projectName = "${project_name}"
}
}

public func == (lhs: TemplateString.Token, rhs: TemplateString.Token) -> Bool {
switch (lhs, rhs) {
case (.projectName, .projectName):
return true
}
}
60 changes: 56 additions & 4 deletions Sources/ProjectDescription/TuistConfig.swift
@@ -1,22 +1,74 @@
import Foundation

/// This model allows to configure Tuist.
public class TuistConfig: Codable {
public class TuistConfig: Encodable, Decodable, Equatable {
/// Contains options related to the project generation.
///
/// - generateManifestElement: When passed, Tuist generates the projects, targets and schemes to compile the project manifest.
public enum GenerationOption: String, Codable {
/// - xcodeProjectName(TemplateString): When passed, Tuist generates the project with the specific name on disk instead of using the project name.
public enum GenerationOptions: Encodable, Decodable, Equatable {
case generateManifest
case xcodeProjectName(TemplateString)
}

/// Generation options.
public let generationOptions: [GenerationOption]
public let generationOptions: [GenerationOptions]

/// Initializes the tuist cofiguration.
///
/// - Parameter generationOptions: Generation options.
public init(generationOptions: [GenerationOption]) {
public init(generationOptions: [GenerationOptions]) {
self.generationOptions = generationOptions
dumpIfNeeded(self)
}
}

extension TuistConfig.GenerationOptions {
enum CodingKeys: String, CodingKey {
case generateManifest
case xcodeProjectName
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

if container.allKeys.contains(.generateManifest), try container.decodeNil(forKey: .generateManifest) == false {
self = .generateManifest
return
}
if container.allKeys.contains(.xcodeProjectName), try container.decodeNil(forKey: .xcodeProjectName) == false {
var associatedValues = try container.nestedUnkeyedContainer(forKey: .xcodeProjectName)
let templateProjectName = try associatedValues.decode(TemplateString.self)
self = .xcodeProjectName(templateProjectName)
return
}
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown enum case"))
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .generateManifest:
_ = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .generateManifest)
case let .xcodeProjectName(templateProjectName):
var associatedValues = container.nestedUnkeyedContainer(forKey: .xcodeProjectName)
try associatedValues.encode(templateProjectName)
}
}
}

public func == (lhs: TuistConfig, rhs: TuistConfig) -> Bool {
guard lhs.generationOptions == rhs.generationOptions else { return false }
return true
}

public func == (lhs: TuistConfig.GenerationOptions, rhs: TuistConfig.GenerationOptions) -> Bool {
switch (lhs, rhs) {
case (.generateManifest, .generateManifest):
return true
case let (.xcodeProjectName(lhs), .xcodeProjectName(rhs)):
return lhs.rawString == rhs.rawString
default: return false
}
}
4 changes: 1 addition & 3 deletions Sources/TuistGenerator/Generator/Generator.swift
Expand Up @@ -88,7 +88,6 @@ public class Generator: Generating {

public func generateProject(at path: AbsolutePath) throws -> AbsolutePath {
let (graph, project) = try graphLoader.loadProject(path: path)

let generatedProject = try projectGenerator.generate(project: project,
graph: graph,
sourceRootPath: path)
Expand All @@ -100,7 +99,7 @@ public class Generator: Generating {
let tuistConfig = try graphLoader.loadTuistConfig(path: path)
let (graph, project) = try graphLoader.loadProject(path: path)

let workspace = Workspace(name: project.name,
let workspace = Workspace(name: project.fileName,
projects: graph.projectPaths,
additionalFiles: workspaceFiles.map(FileElement.file))

Expand All @@ -114,7 +113,6 @@ public class Generator: Generating {
workspaceFiles: [AbsolutePath]) throws -> AbsolutePath {
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)
let tuistConfig = try graphLoader.loadTuistConfig(path: path)

let updatedWorkspace = workspace
.merging(projects: graph.projectPaths)
.adding(files: workspaceFiles)
Expand Down
3 changes: 2 additions & 1 deletion Sources/TuistGenerator/Generator/ProjectGenerator.swift
Expand Up @@ -87,7 +87,8 @@ final class ProjectGenerator: ProjectGenerating {

// Getting the path.
let sourceRootPath = sourceRootPath ?? project.path
let xcodeprojPath = sourceRootPath.appending(component: "\(project.name).xcodeproj")

let xcodeprojPath = sourceRootPath.appending(component: "\(project.fileName).xcodeproj")

// Derived files
let deleteOldDerivedFiles = try derivedFileGenerator.generate(project: project, sourceRootPath: sourceRootPath)
Expand Down
5 changes: 5 additions & 0 deletions Sources/TuistGenerator/Models/Project.swift
Expand Up @@ -11,6 +11,9 @@ public class Project: Equatable, CustomStringConvertible {
/// Project name.
public let name: String

/// Project file name.
public let fileName: String

/// Project targets.
public let targets: [Target]

Expand Down Expand Up @@ -40,13 +43,15 @@ public class Project: Equatable, CustomStringConvertible {
/// *(Those won't be included in any build phases)*
public init(path: AbsolutePath,
name: String,
fileName: String? = nil,
settings: Settings,
filesGroup: ProjectGroup,
targets: [Target],
schemes: [Scheme],
additionalFiles: [FileElement] = []) {
self.path = path
self.name = name
self.fileName = fileName ?? name
self.targets = targets
self.schemes = schemes
self.settings = settings
Expand Down
13 changes: 12 additions & 1 deletion Sources/TuistGenerator/Models/TuistConfig.swift
Expand Up @@ -7,8 +7,9 @@ public class TuistConfig: Equatable, Hashable {
/// Contains options related to the project generation.
///
/// - generateManifestElement: When passed, Tuist generates the projects, targets and schemes to compile the project manifest.
public enum GenerationOption: String, Codable, Equatable, Hashable {
public enum GenerationOption: Hashable, Equatable {
case generateManifest
case xcodeProjectName(String)
}

/// Generation options.
Expand Down Expand Up @@ -49,3 +50,13 @@ public class TuistConfig: Equatable, Hashable {
return lhs.generationOptions == rhs.generationOptions
}
}

public func == (lhs: TuistConfig.GenerationOption, rhs: TuistConfig.GenerationOption) -> Bool {
switch (lhs, rhs) {
case (.generateManifest, .generateManifest):
return true
case let (.xcodeProjectName(lhs), .xcodeProjectName(rhs)):
return lhs == rhs
default: return false
}
}
69 changes: 57 additions & 12 deletions Sources/TuistKit/Generator/GeneratorModelLoader.swift
Expand Up @@ -53,24 +53,18 @@ class GeneratorModelLoader: GeneratorModelLoading {
/// - Throws: Error encountered during the loading process (e.g. Missing project)
func loadProject(at path: AbsolutePath) throws -> TuistGenerator.Project {
let manifest = try manifestLoader.loadProject(at: path)
let tuistConfig = try loadTuistConfig(at: path)

try manifestLinter.lint(project: manifest)
.printAndThrowIfNeeded(printer: printer)

let project = try TuistGenerator.Project.from(manifest: manifest,
path: path,
fileHandler: fileHandler,
printer: printer)
let tuistConfig = try loadTuistConfig(at: path)
printer: printer,
tuistConfig: tuistConfig)

if let manifestTargetGenerator = manifestTargetGenerator, tuistConfig.generationOptions.contains(.generateManifest) {
let manifestTarget = try manifestTargetGenerator.generateManifestTarget(for: project.name,
at: path)
return project.adding(target: manifestTarget)

} else {
return project
}
return try enriched(model: project, with: tuistConfig)
}

func loadWorkspace(at path: AbsolutePath) throws -> TuistGenerator.Workspace {
Expand Down Expand Up @@ -114,6 +108,42 @@ class GeneratorModelLoader: GeneratorModelLoading {
return locateDirectoryTraversingParents(from: from.parentDirectory, path: path, fileHandler: fileHandler)
}
}

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.


// Manifest target
if let manifestTargetGenerator = manifestTargetGenerator, config.generationOptions.contains(.generateManifest) {
let manifestTarget = try manifestTargetGenerator.generateManifestTarget(for: enrichedModel.name,
at: enrichedModel.path)
enrichedModel = enrichedModel.adding(target: manifestTarget)
}

// Xcode project file name
let xcodeFileName = xcodeFileNameOverride(from: config, for: model)
enrichedModel = enrichedModel.replacing(fileName: xcodeFileName)

return enrichedModel
}

private func xcodeFileNameOverride(from config: TuistGenerator.TuistConfig,
for model: TuistGenerator.Project) -> String? {
var xcodeFileName = config.generationOptions.compactMap { item -> String? in
switch item {
case let .xcodeProjectName(projectName):
return projectName.description
default:
return nil
}
}.first

let projectNameTemplate = TemplateString.Token.projectName.rawValue
xcodeFileName = xcodeFileName?.replacingOccurrences(of: projectNameTemplate,
with: model.name)

return xcodeFileName
}
}

extension TuistGenerator.TuistConfig {
Expand All @@ -125,11 +155,13 @@ extension TuistGenerator.TuistConfig {
}

extension TuistGenerator.TuistConfig.GenerationOption {
static func from(manifest: ProjectDescription.TuistConfig.GenerationOption,
static func from(manifest: ProjectDescription.TuistConfig.GenerationOptions,
path _: AbsolutePath) throws -> TuistGenerator.TuistConfig.GenerationOption {
switch manifest {
case .generateManifest:
return .generateManifest
case let .xcodeProjectName(templateString):
return .xcodeProjectName(templateString.description)
}
}
}
Expand Down Expand Up @@ -218,7 +250,8 @@ extension TuistGenerator.Project {
static func from(manifest: ProjectDescription.Project,
path: AbsolutePath,
fileHandler: FileHandling,
printer: Printing) throws -> TuistGenerator.Project {
printer: Printing,
tuistConfig _: TuistGenerator.TuistConfig) throws -> TuistGenerator.Project {
let name = manifest.name
let settings = manifest.settings.map { TuistGenerator.Settings.from(manifest: $0, path: path) }
let targets = try manifest.targets.map {
Expand Down Expand Up @@ -249,12 +282,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.

return Project(path: path,
name: name,
fileName: fileName,
settings: settings,
filesGroup: filesGroup,
targets: targets + [target],
schemes: schemes,
additionalFiles: additionalFiles)
}

func replacing(fileName: String?) -> TuistGenerator.Project {
return Project(path: path,
name: name,
fileName: fileName,
settings: settings,
filesGroup: filesGroup,
targets: targets,
schemes: schemes,
additionalFiles: additionalFiles)
}
}

extension TuistGenerator.Target {
Expand Down
13 changes: 13 additions & 0 deletions Tests/ProjectDescriptionTests/TuistConfigTests.swift
@@ -0,0 +1,13 @@
import Foundation
import XCTest
@testable import ProjectDescription

final class TuistConfigTests: XCTestCase {
func test_tuistconfig_toJSON() throws {
let tuistConfig = TuistConfig(generationOptions:
[.generateManifest,
.xcodeProjectName("someprefix-\(.projectName)")])

XCTAssertCodable(tuistConfig)
}
}
25 changes: 25 additions & 0 deletions Tests/TuistGeneratorTests/Generator/ProjectGeneratorTests.swift
Expand Up @@ -139,4 +139,29 @@ final class ProjectGeneratorTests: XCTestCase {

}, "Test target is missing from target attributes.")
}

func test_generate_testUsingFileName() throws {
// Given
let project = Project.test(path: fileHandler.currentPath,
name: "Project",
fileName: "SomeAwesomeName",
targets: [])
try fileHandler.touch(fileHandler.currentPath.appending(component: "Project.swift"))
let target = Target.test()
let cache = GraphLoaderCache()
cache.add(project: project)
let graph = Graph.test(entryPath: fileHandler.currentPath,
cache: cache,
entryNodes: [TargetNode(project: project,
target: target,
dependencies: [])])

// When
let got = try subject.generate(project: project, graph: graph)

// Then
XCTAssertTrue(fileHandler.exists(got.path))
XCTAssertEqual(got.path.components.last, "SomeAwesomeName.xcodeproj")
XCTAssertEqual(project.name, "Project")
}
}