Skip to content

Commit

Permalink
Add compatible Xcodes option to the TuistConfig (#476)
Browse files Browse the repository at this point in the history
* Add model changes to ProjectDescription

* Add models to the TuistGenerator target

* Create Xcode & XcodeController to interact with local Xcode installations

* Implement TuistConfigLinter

* Add acceptance test

* Support initializing CompatibleXcodeVersions with a string literal

* Add documentation

* Update CHANGELOG

* Address comments

* Fix imports

* Rename TuistConfigLinter to EnvironmentLinter

* Some style fixes
  • Loading branch information
Pedro Piñera Buendía committed Aug 7, 2019
1 parent 1aed58f commit 2b09f5c
Show file tree
Hide file tree
Showing 37 changed files with 774 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- 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
- `TuistConfig.compatibleXcodeVersions` support https://github.com/tuist/tuist/pull/476 by @pepibumur.

### Fixed

Expand Down
58 changes: 58 additions & 0 deletions Sources/ProjectDescription/CompatibleXcodeVersions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

/// Enum that represents all the Xcode versions that a project or set of projects is compatible with.
public enum CompatibleXcodeVersions: ExpressibleByArrayLiteral, ExpressibleByStringLiteral, Codable, Equatable {
/// The project supports all Xcode versions.
case all

/// List of versions that are supported by the project.
case list([String])

// MARK: - ExpressibleByArrayLiteral

public init(arrayLiteral elements: [String]) {
self = .list(elements)
}

public init(arrayLiteral elements: String...) {
self = .list(elements)
}

enum CodignKeys: String, CodingKey {
case type
case value
}

// MARK: - ExpressibleByStringLiteral

public init(stringLiteral value: String) {
self = .list([value])
}

// MARK: - Codable

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodignKeys.self)
switch self {
case .all:
try container.encode("all", forKey: .type)
case let .list(versions):
try container.encode("list", forKey: .type)
try container.encode(versions, forKey: .value)
}
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodignKeys.self)
let type = try container.decode(String.self, forKey: .type)

switch type {
case "all":
self = .all
case "list":
self = .list(try container.decode([String].self, forKey: .value))
default:
throw DecodingError.dataCorruptedError(forKey: CodignKeys.type, in: container, debugDescription: "Invalid type \(type)")
}
}
}
11 changes: 9 additions & 2 deletions Sources/ProjectDescription/TuistConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ public class TuistConfig: Encodable, Decodable, Equatable {
/// Generation options.
public let generationOptions: [GenerationOptions]

/// List of Xcode versions that the project supports.
public let compatibleXcodeVersions: CompatibleXcodeVersions

/// Initializes the tuist cofiguration.
///
/// - Parameter generationOptions: Generation options.
public init(generationOptions: [GenerationOptions]) {
/// - Parameters:
/// - compatibleXcodeVersions: .
/// - generationOptions: List of Xcode versions that the project supports. An empty list means that
public init(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
generationOptions: [GenerationOptions]) {
self.generationOptions = generationOptions
self.compatibleXcodeVersions = compatibleXcodeVersions
dumpIfNeeded(self)
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/TuistCore/Linter/LintingIssue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ public extension Array where Element == LintingIssue {

if !errorIssues.isEmpty {
let prefix = !warningIssues.isEmpty ? "\n" : ""
printer.print("\(prefix)The following critical issues have been found:", color: .red)
printer.print("\(prefix)The following critical issues have been found:", output: .standardError)
let message = errorIssues.map { " - \($0.description)" }.joined(separator: "\n")
printer.print(message)
printer.print(message, output: .standardError)

throw LintingError()
}
Expand Down
26 changes: 21 additions & 5 deletions Sources/TuistCore/Utils/Printer.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import Basic
import Foundation

public enum PrinterOutput {
case standardOputput
case standardError
}

public protocol Printing: AnyObject {
func print(_ text: String)
func print(_ text: String, output: PrinterOutput)
func print(_ text: String, color: TerminalController.Color)
func print(section: String)
func print(subsection: String)
Expand All @@ -25,7 +31,17 @@ public class Printer: Printing {
// MARK: - Public

public func print(_ text: String) {
let writer = InteractiveWriter.stdout
print(text, output: .standardOputput)
}

public func print(_ text: String, output: PrinterOutput) {
let writer: InteractiveWriter!
if output == .standardOputput {
writer = .stdout
} else {
writer = .stderr
}

writer.write(text)
writer.write("\n")
}
Expand Down Expand Up @@ -56,21 +72,21 @@ public class Printer: Printing {
public func print(deprecation: String) {
let writer = InteractiveWriter.stdout
writer.write("Deprecated: ", inColor: .yellow, bold: true)
writer.write(deprecation, inColor: .yellow, bold: false)
writer.write(deprecation, inColor: .yellow, bold: true)
writer.write("\n")
}

public func print(warning: String) {
let writer = InteractiveWriter.stdout
writer.write("Warning: ", inColor: .yellow, bold: true)
writer.write(warning, inColor: .yellow, bold: false)
writer.write(warning, inColor: .yellow, bold: true)
writer.write("\n")
}

public func print(errorMessage: String) {
let writer = InteractiveWriter.stderr
writer.write("Error: ", inColor: .red, bold: true)
writer.write(errorMessage, inColor: .red, bold: false)
writer.write(errorMessage, inColor: .red, bold: true)
writer.write("\n")
}

Expand All @@ -91,7 +107,7 @@ public class Printer: Printing {
///
/// If underlying stream is a not tty, the string will be written in without any
/// formatting.
private final class InteractiveWriter {
final class InteractiveWriter {
/// The standard error writer.
static let stderr = InteractiveWriter(stream: stderrStream)

Expand Down
51 changes: 51 additions & 0 deletions Sources/TuistCore/Xcode/Xcode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Basic
import Foundation

public struct Xcode {
/// It represents the content of the Info.plist file inside the Xcode app bundle.
public struct InfoPlist: Codable {
/// App version number (e.g. 10.3)
public let version: String

/// Initializes the InfoPlist object with its attributes.
///
/// - Parameter version: Version.
public init(version: String) {
self.version = version
}

enum CodingKeys: String, CodingKey {
case version = "CFBundleShortVersionString"
}
}

/// Path to the Xcode app bundle.
public let path: AbsolutePath

/// Info plist content.
public let infoPlist: InfoPlist

/// Initializes an Xcode instance by reading it from a local Xcode.app bundle.
///
/// - Parameter path: Path to a local Xcode.app bundle.
/// - Returns: Initialized Xcode instance.
/// - Throws: An error if the local installation can't be read.
static func read(path: AbsolutePath) throws -> Xcode {
let infoPlistPath = path.appending(RelativePath("Contents/Info.plist"))
let plistDecoder = PropertyListDecoder()
let data = try Data(contentsOf: infoPlistPath.url)
let infoPlist = try plistDecoder.decode(InfoPlist.self, from: data)

return Xcode(path: path, infoPlist: infoPlist)
}

/// Initializes an instance of Xcode which represents a local installation of Xcode
///
/// - Parameters:
/// - path: Path to the Xcode app bundle.
public init(path: AbsolutePath,
infoPlist: InfoPlist) {
self.path = path
self.infoPlist = infoPlist
}
}
37 changes: 37 additions & 0 deletions Sources/TuistCore/Xcode/XcodeController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Basic
import Foundation

public protocol XcodeControlling {
/// Returns the selected Xcode. It uses xcode-select to determine
/// the Xcode that is selected in the environment.
///
/// - Returns: Selected Xcode.
/// - Throws: An error if it can't be obtained.
func selected() throws -> Xcode?
}

public class XcodeController: XcodeControlling {
/// Instance to run commands in the system.
let system: Systeming

/// Initializes the controller with its attributes
///
/// - Parameters:
/// - system: Instance to run commands in the system.
public init(system: Systeming = System()) {
self.system = system
}

/// Returns the selected Xcode. It uses xcode-select to determine
/// the Xcode that is selected in the environment.
///
/// - Returns: Selected Xcode.
/// - Throws: An error if it can't be obtained.
public func selected() throws -> Xcode? {
// e.g. /Applications/Xcode.app/Contents/Developer
guard let path = try? system.capture(["xcode-select", "-p"]).spm_chomp() else {
return nil
}
return try Xcode.read(path: AbsolutePath(path).parentDirectory.parentDirectory)
}
}
4 changes: 4 additions & 0 deletions Sources/TuistCoreTesting/Extensions/XCTestCase+Extras.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,8 @@ public extension XCTestCase {
XCTFail("Failed comparing the subject to the given JSON. Has the JSON the right format?")
}
}

func XCTEmpty<S>(_ array: [S], file: StaticString = #file, line: UInt = #line) {
XCTAssertTrue(array.isEmpty, "Expected array to be empty but it's not. It contains the following elements: \(array)", file: file, line: line)
}
}
11 changes: 10 additions & 1 deletion Sources/TuistCoreTesting/Utils/MockPrinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,17 @@ public final class MockPrinter: Printing {
public var printDeprecationArgs: [String] = []

public func print(_ text: String) {
print(text, output: .standardOputput)
}

public func print(_ text: String, output: PrinterOutput) {
printArgs.append(text)
standardOutput.append(text)

if output == .standardOputput {
standardOutput.append("\(text)\n")
} else {
standardError.append("\(text)\n")
}
}

public func print(_ text: String, color: TerminalController.Color) {
Expand Down
16 changes: 16 additions & 0 deletions Sources/TuistCoreTesting/Xcode/Mocks/MockXcodeController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation

import TuistCore

final class MockXcodeController: XcodeControlling {
var selectedStub: Result<Xcode, Error>?

func selected() throws -> Xcode? {
guard let selectedStub = selectedStub else { return nil }

switch selectedStub {
case let .failure(error): throw error
case let .success(xcode): return xcode
}
}
}
17 changes: 17 additions & 0 deletions Sources/TuistCoreTesting/Xcode/TestData/Xcode+TestData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Basic
import Foundation

import TuistCore

extension Xcode {
static func test(path: AbsolutePath = AbsolutePath("/Applications/Xcode.app"),
infoPlist: Xcode.InfoPlist = .test()) -> Xcode {
return Xcode(path: path, infoPlist: infoPlist)
}
}

extension Xcode.InfoPlist {
static func test(version: String = "3.2.1") -> Xcode.InfoPlist {
return Xcode.InfoPlist(version: version)
}
}
23 changes: 18 additions & 5 deletions Sources/TuistGenerator/Generator/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public class Generator: Generating {
private let workspaceGenerator: WorkspaceGenerating
private let projectGenerator: ProjectGenerating

/// Instance to lint the Tuist configuration against the system.
private let environmentLinter: EnvironmentLinting

public convenience init(system: Systeming = System(),
printer: Printing = Printer(),
fileHandler: FileHandling = FileHandler(),
Expand All @@ -65,6 +68,7 @@ public class Generator: Generating {
printer: printer,
system: system,
fileHandler: fileHandler)
let environmentLinter = EnvironmentLinter()
let workspaceStructureGenerator = WorkspaceStructureGenerator(fileHandler: fileHandler)
let cocoapodsInteractor = CocoaPodsInteractor()
let workspaceGenerator = WorkspaceGenerator(system: system,
Expand All @@ -75,18 +79,24 @@ public class Generator: Generating {
cocoapodsInteractor: cocoapodsInteractor)
self.init(graphLoader: graphLoader,
workspaceGenerator: workspaceGenerator,
projectGenerator: projectGenerator)
projectGenerator: projectGenerator,
environmentLinter: environmentLinter)
}

init(graphLoader: GraphLoading,
workspaceGenerator: WorkspaceGenerating,
projectGenerator: ProjectGenerating) {
projectGenerator: ProjectGenerating,
environmentLinter: EnvironmentLinting) {
self.graphLoader = graphLoader
self.workspaceGenerator = workspaceGenerator
self.projectGenerator = projectGenerator
self.environmentLinter = environmentLinter
}

public func generateProject(at path: AbsolutePath) throws -> AbsolutePath {
let tuistConfig = try graphLoader.loadTuistConfig(path: path)
try environmentLinter.lint(config: tuistConfig)

let (graph, project) = try graphLoader.loadProject(path: path)
let generatedProject = try projectGenerator.generate(project: project,
graph: graph,
Expand All @@ -97,9 +107,10 @@ public class Generator: Generating {
public func generateProjectWorkspace(at path: AbsolutePath,
workspaceFiles: [AbsolutePath]) throws -> AbsolutePath {
let tuistConfig = try graphLoader.loadTuistConfig(path: path)
let (graph, project) = try graphLoader.loadProject(path: path)
try environmentLinter.lint(config: tuistConfig)

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

Expand All @@ -111,8 +122,10 @@ public class Generator: Generating {

public func generateWorkspace(at path: AbsolutePath,
workspaceFiles: [AbsolutePath]) throws -> AbsolutePath {
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)
let tuistConfig = try graphLoader.loadTuistConfig(path: path)
try environmentLinter.lint(config: tuistConfig)
let (graph, workspace) = try graphLoader.loadWorkspace(path: path)

let updatedWorkspace = workspace
.merging(projects: graph.projectPaths)
.adding(files: workspaceFiles)
Expand Down

0 comments on commit 2b09f5c

Please sign in to comment.