Skip to content

Commit

Permalink
Use Custom File Name for .xcodeproj at Model Level (#462)
Browse files Browse the repository at this point in the history
* using custom file name

* swiftformat

* use file name from model

* changelog

* fix test

* enum suffix, prefix, test, and new fixture

* acceptance test

* swiftformat

* new api

* docs and clean ups

* swiftformat and clean up

* expose less at generator level, tuist config tests for code coverage

* more code coverage?

* clean ups

* clean ups

* pr suggestions

* swift format

* fixes tests from changes in master
  • Loading branch information
adamkhazi authored and Pedro Piñera Buendía committed Aug 7, 2019
1 parent 2285432 commit 1aed58f
Show file tree
Hide file tree
Showing 16 changed files with 330 additions and 24 deletions.
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

// 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 {
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")
}
}

0 comments on commit 1aed58f

Please sign in to comment.