Add compatible Xcodes option to the TuistConfig (#476)

* 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
This commit is contained in:
Pedro Piñera Buendía 2019-08-07 19:44:44 +02:00 committed by GitHub
parent 1aed58f4ec
commit 2b09f5cfa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 774 additions and 41 deletions

View File

@ -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

View File

@ -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)")
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
@ -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")
}
@ -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")
}
@ -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)

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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(),
@ -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,
@ -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,
@ -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))
@ -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)

View File

@ -0,0 +1,67 @@
import Foundation
import TuistCore
protocol EnvironmentLinting {
/// Lints a given Tuist configuration.
///
/// - Parameter config: Tuist configuration to be linted against the system.
/// - Throws: An error if the validation fails.
func lint(config: TuistConfig) throws
}
class EnvironmentLinter: EnvironmentLinting {
/// Xcode controller.
let xcodeController: XcodeControlling
/// Printer to output messages to the user.
let printer: Printing
/// Initialies the linter.
///
/// - Parameters:
/// - xcodeController: Xcode controller.
/// - printer: Printer to output messages to the user.
init(xcodeController: XcodeControlling = XcodeController(),
printer: Printing = Printer()) {
self.xcodeController = xcodeController
self.printer = printer
}
/// Lints a given Tuist configuration.
///
/// - Parameter config: Tuist configuration to be linted against the system.
/// - Throws: An error if the validation fails.
func lint(config: TuistConfig) throws {
var issues = [LintingIssue]()
issues.append(contentsOf: try lintXcodeVersion(config: config))
try issues.printAndThrowIfNeeded(printer: printer)
}
/// Returns a linting issue if the selected version of Xcode is not compatible with the
/// compatibility defined using the compatibleXcodeVersions attribute.
///
/// - Parameter config: Tuist configuration.
/// - Returns: An array with a linting issue if the selected version is not compatible.
/// - Throws: An error if there's an error obtaining the selected Xcode version.
func lintXcodeVersion(config: TuistConfig) throws -> [LintingIssue] {
guard case let CompatibleXcodeVersions.list(compatibleVersions) = config.compatibleXcodeVersions else {
return []
}
guard let xcode = try xcodeController.selected() else {
return []
}
let version = xcode.infoPlist.version
if !compatibleVersions.contains(version) {
let versions = compatibleVersions.joined(separator: ", ")
let message = "The project, which only supports the versions of Xcode \(versions), is not compatible with your selected version of Xcode, \(version)"
return [LintingIssue(reason: message, severity: .error)]
} else {
return []
}
}
}

View File

@ -0,0 +1,20 @@
import Foundation
/// Enum that represents all the Xcode versions that a project or set of projects is compatible with.
public enum CompatibleXcodeVersions: Equatable, Hashable, ExpressibleByArrayLiteral {
/// 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)
}
}

View File

@ -15,16 +15,23 @@ public class TuistConfig: Equatable, Hashable {
/// Generation options.
public let generationOptions: [GenerationOption]
/// List of Xcode versions the project or set of projects is compatible with.
public let compatibleXcodeVersions: CompatibleXcodeVersions
/// Returns the default Tuist configuration.
public static var `default`: TuistConfig {
return TuistConfig(generationOptions: [.generateManifest])
return TuistConfig(compatibleXcodeVersions: .all,
generationOptions: [.generateManifest])
}
/// Initializes the tuist cofiguration.
///
/// - Parameters:
/// - compatibleXcodeVersions: List of Xcode versions the project or set of projects is compatible with.
/// - generationOptions: Generation options.
public init(generationOptions: [GenerationOption]) {
public init(compatibleXcodeVersions: CompatibleXcodeVersions,
generationOptions: [GenerationOption]) {
self.compatibleXcodeVersions = compatibleXcodeVersions
self.generationOptions = generationOptions
}

View File

@ -150,7 +150,10 @@ extension TuistGenerator.TuistConfig {
static func from(manifest: ProjectDescription.TuistConfig,
path: AbsolutePath) throws -> TuistGenerator.TuistConfig {
let generationOptions = try manifest.generationOptions.map { try TuistGenerator.TuistConfig.GenerationOption.from(manifest: $0, path: path) }
return TuistGenerator.TuistConfig(generationOptions: generationOptions)
let compatibleXcodeVersions = TuistGenerator.CompatibleXcodeVersions.from(manifest: manifest.compatibleXcodeVersions)
return TuistGenerator.TuistConfig(compatibleXcodeVersions: compatibleXcodeVersions,
generationOptions: generationOptions)
}
}
@ -166,6 +169,17 @@ extension TuistGenerator.TuistConfig.GenerationOption {
}
}
extension TuistGenerator.CompatibleXcodeVersions {
static func from(manifest: ProjectDescription.CompatibleXcodeVersions) -> TuistGenerator.CompatibleXcodeVersions {
switch manifest {
case .all:
return .all
case let .list(versions):
return .list(versions)
}
}
}
extension TuistGenerator.Workspace {
static func from(manifest: ProjectDescription.Workspace,
path: AbsolutePath,

View File

@ -0,0 +1,23 @@
import Foundation
import XCTest
@testable import ProjectDescription
@testable import TuistCoreTesting
final class CompatibleXcodeVersionsTests: XCTestCase {
func test_codable_when_all() {
// Given
let subject = CompatibleXcodeVersions.all
// Then
XCTAssertCodable(subject)
}
func test_codable_when_list() {
// Given
let subject = CompatibleXcodeVersions.list(["10.3"])
// Then
XCTAssertCodable(subject)
}
}

View File

@ -25,13 +25,14 @@ final class LintingIssueTests: XCTestCase {
XCTAssertThrowsError(try [first, second].printAndThrowIfNeeded(printer: printer))
XCTAssertEqual(printer.printWithColorArgs.first?.0, "The following issues have been found:")
XCTAssertEqual(printer.printWithColorArgs.first?.1, .yellow)
XCTAssertEqual(printer.printArgs.first, " - warning")
XCTAssertEqual(printer.printWithColorArgs.last?.0, "\nThe following critical issues have been found:")
XCTAssertEqual(printer.printWithColorArgs.last?.1, .red)
XCTAssertEqual(printer.printArgs.last, " - error")
XCTAssertTrue(printer.standardOutput.contains("""
The following issues have been found:
- warning
"""))
XCTAssertTrue(printer.standardError.contains("""
The following critical issues have been found:
- error
"""))
}
func test_printAndThrowIfNeeded_whenErrorsOnly() throws {
@ -40,8 +41,9 @@ final class LintingIssueTests: XCTestCase {
XCTAssertThrowsError(try [first].printAndThrowIfNeeded(printer: printer))
XCTAssertEqual(printer.printWithColorArgs.last?.0, "The following critical issues have been found:")
XCTAssertEqual(printer.printWithColorArgs.last?.1, .red)
XCTAssertEqual(printer.printArgs.last, " - error")
XCTAssertTrue(printer.standardError.contains("""
The following critical issues have been found:
- error
"""))
}
}

View File

@ -0,0 +1,48 @@
import Foundation
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
final class XcodeControllerTests: XCTestCase {
var system: MockSystem!
var subject: XcodeController!
var fileHandler: MockFileHandler!
override func setUp() {
super.setUp()
system = MockSystem()
fileHandler = try! MockFileHandler()
subject = XcodeController(system: system)
}
func test_selected_when_xcodeSelectDoesntReturnThePath() throws {
// Given
system.errorCommand(["xcode-select", "-p"])
// When
let xcode = try subject.selected()
// Then
XCTAssertNil(xcode)
}
func test_selected_when_xcodeSelectReturnsThePath() throws {
// Given
let contentsPath = fileHandler.currentPath.appending(component: "Contents")
try fileHandler.createFolder(contentsPath)
let infoPlistPath = contentsPath.appending(component: "Info.plist")
let developerPath = contentsPath.appending(component: "Developer")
let infoPlist = Xcode.InfoPlist(version: "3.2.1")
let infoPlistData = try PropertyListEncoder().encode(infoPlist)
try infoPlistData.write(to: infoPlistPath.url)
system.succeedCommand(["xcode-select", "-p"], output: developerPath.pathString)
// When
let xcode = try subject.selected()
// Then
XCTAssertNotNil(xcode)
}
}

View File

@ -0,0 +1,33 @@
import Foundation
import XCTest
@testable import TuistCore
@testable import TuistCoreTesting
final class XcodeTests: XCTestCase {
var plistEncoder: PropertyListEncoder!
var fileHandler: MockFileHandler!
override func setUp() {
super.setUp()
plistEncoder = PropertyListEncoder()
fileHandler = try! MockFileHandler()
}
func test_read() throws {
// Given
let infoPlist = Xcode.InfoPlist(version: "3.2.1")
let infoPlistData = try plistEncoder.encode(infoPlist)
let contentsPath = fileHandler.currentPath.appending(component: "Contents")
try fileHandler.createFolder(contentsPath)
let infoPlistPath = contentsPath.appending(component: "Info.plist")
try infoPlistData.write(to: infoPlistPath.url)
// When
let xcode = try Xcode.read(path: fileHandler.currentPath)
// Then
XCTAssertEqual(xcode.infoPlist.version, "3.2.1")
XCTAssertEqual(xcode.path, fileHandler.currentPath)
}
}

View File

@ -7,16 +7,19 @@ class GeneratorTests: XCTestCase {
var workspaceGenerator: MockWorkspaceGenerator!
var projectGenerator: MockProjectGenerator!
var graphLoader: MockGraphLoader!
var environmentLinter: MockEnvironmentLinter!
var subject: Generator!
override func setUp() {
graphLoader = MockGraphLoader()
workspaceGenerator = MockWorkspaceGenerator()
projectGenerator = MockProjectGenerator()
environmentLinter = MockEnvironmentLinter()
subject = Generator(graphLoader: graphLoader,
workspaceGenerator: workspaceGenerator,
projectGenerator: projectGenerator)
projectGenerator: projectGenerator,
environmentLinter: environmentLinter)
}
// MARK: - Tests

View File

@ -0,0 +1,68 @@
import Foundation
import TuistCore
import XCTest
@testable import TuistCoreTesting
@testable import TuistGenerator
final class EnvironmentLinterTests: XCTestCase {
var xcodeController: MockXcodeController!
var printer: MockPrinter!
var subject: EnvironmentLinter!
override func setUp() {
super.setUp()
xcodeController = MockXcodeController()
printer = MockPrinter()
subject = EnvironmentLinter(xcodeController: xcodeController,
printer: printer)
}
func test_lintXcodeVersion_returnsALintingIssue_when_theVersionsOfXcodeAreIncompatible() throws {
// Given
let config = TuistConfig.test(compatibleXcodeVersions: .list(["3.2.1"]))
xcodeController.selectedStub = .success(Xcode.test(infoPlist: .test(version: "4.3.2")))
// When
let got = try subject.lintXcodeVersion(config: config)
// Then
let expectedMessage = "The project, which only supports the versions of Xcode 3.2.1, is not compatible with your selected version of Xcode, 4.3.2"
XCTAssertTrue(got.contains(LintingIssue(reason: expectedMessage, severity: .error)))
}
func test_lintXcodeVersion_doesntReturnIssues_whenAllVersionsAreSupported() throws {
// Given
let config = TuistConfig.test(compatibleXcodeVersions: .all)
xcodeController.selectedStub = .success(Xcode.test(infoPlist: .test(version: "4.3.2")))
// When
let got = try subject.lintXcodeVersion(config: config)
// Then
XCTEmpty(got)
}
func test_lintXcodeVersion_doesntReturnIssues_whenThereIsNoSelectedXcode() throws {
// Given
let config = TuistConfig.test(compatibleXcodeVersions: .list(["3.2.1"]))
// When
let got = try subject.lintXcodeVersion(config: config)
// Then
XCTEmpty(got)
}
func test_lintXcodeVersion_throws_when_theSelectedXcodeCantBeObtained() throws {
// Given
let config = TuistConfig.test(compatibleXcodeVersions: .list(["3.2.1"]))
let error = NSError.test()
xcodeController.selectedStub = .failure(error)
// Then
XCTAssertThrowsError(try subject.lintXcodeVersion(config: config)) {
XCTAssertEqual($0 as NSError, error)
}
}
}

View File

@ -0,0 +1,15 @@
import Foundation
@testable import TuistGenerator
final class MockEnvironmentLinter: EnvironmentLinting {
var lintStub: Error?
var lintArgs: [TuistConfig] = []
func lint(config: TuistConfig) throws {
lintArgs.append(config)
if let error = lintStub {
throw error
}
}
}

View File

@ -3,7 +3,9 @@ import Foundation
@testable import TuistGenerator
extension TuistConfig {
static func test(generationOptions: [GenerationOption] = []) -> TuistConfig {
return TuistConfig(generationOptions: generationOptions)
static func test(compatibleXcodeVersions: CompatibleXcodeVersions = .all,
generationOptions: [GenerationOption] = []) -> TuistConfig {
return TuistConfig(compatibleXcodeVersions: compatibleXcodeVersions,
generationOptions: generationOptions)
}
}

View File

@ -3,6 +3,8 @@ import Foundation
import TuistCore
class MockPrinter: Printing {
func print(_: String, output _: PrinterOutput) {}
func print(_: String) {}
func print(_: String, color _: TerminalController.Color) {}

View File

@ -324,7 +324,8 @@ final class MultipleConfigurationsIntegrationTests: XCTestCase {
}
private func createTuistConfig() -> TuistConfig {
return TuistConfig(generationOptions: [])
return TuistConfig(compatibleXcodeVersions: .all,
generationOptions: [])
}
private func createWorkspace(projects: [String]) -> Workspace {

View File

@ -96,7 +96,8 @@ final class StableXcodeProjIntegrationTests: XCTestCase {
}
private func createTuistConfig() -> TuistConfig {
return TuistConfig(generationOptions: [])
return TuistConfig(compatibleXcodeVersions: .all,
generationOptions: [])
}
private func createWorkspace(projects: [String]) -> Workspace {

View File

@ -120,11 +120,13 @@ final class SetupLoaderTests: XCTestCase {
let expectedOutput = """
The following issues have been found:
- mockup2 warning
"""
let expectedError = """
The following critical issues have been found:
- mockup1 error
- mockup3 error
"""
XCTAssertEqual(printer.standardOutput, expectedOutput)
XCTAssertEqual(printer.standardError, "")
XCTAssertTrue(printer.standardOutput.contains(expectedOutput))
XCTAssertTrue(printer.standardError.contains(expectedError))
}
}

View File

@ -30,6 +30,7 @@ The structure is similar to the project manifest. We need to create a root varia
import ProjectDescription
let config = TuistConfig(
compatibleXcodeVersions: ["10.3"],
generationOptions: [
.generateManifest,
.xcodeProjectName("SomePrefix-\(.projectName)-SomeSuffix")
@ -43,6 +44,15 @@ It allows configuring Tuist and share the configuration across several projects.
<PropertiesTable
props={[
{
name: 'Compatible Xcode versions',
description:
'Set the versions of Xcode that the project is compatible with.',
type: 'CompatibleXcodeVersions',
typeLink: '#compatible-xcode-versions',
optional: true,
default: '.all',
},
{
name: 'Generation options',
description: 'Options to configure the generation of Xcode projects',
@ -54,6 +64,29 @@ It allows configuring Tuist and share the configuration across several projects.
]}
/>
## Compatible Xcode versions
This object represents the versions of Xcode the project is compatible with. If a developer tries to generate a project and its selected Xcode version is not compatible with the project, Tuist will yield an error:
<EnumTable
cases={[
{
case: '.all',
description: 'The project is compatible with any version of Xcode.',
},
{
case: '.list([String])',
description: 'The project is compatible with a list of Xcode versions.',
},
]}
/>
<Message
info
title="ExpressibleByArrayLiteral and ExpressibleByStringLiteral"
description="Note that 'CompatibleXcodeVersions' can also be initialized with a string or array of strings that represent the supported Xcode versions."
/>
## GenerationOption
Generation options allow customizing the generation of Xcode projects.

View File

@ -166,4 +166,10 @@ Scenario: The project is an iOS application with CocoaPods dependencies (ios_app
And I have a working directory
Then I copy the fixture ios_app_with_pods into the working directory
Then tuist generates the project
Then I should be able to build the scheme App
Then I should be able to build the scheme App
Scenario: The project is an iOS application with an incompatible Xcode version (ios_app_with_incompatible_xcode)
Given that tuist is available
And I have a working directory
Then I copy the fixture ios_app_with_incompatible_xcode into the working directory
Then tuist generates yields error "The project, which only supports the versions of Xcode 3.2.1, is not compatible with your selected version of Xcode"

View File

@ -14,13 +14,21 @@ Then(/tuist sets up the project/) do
@workspace_path = Dir.glob(File.join(@dir, "*.xcworkspace")).first
end
Then(/tuist generates reports error "(.+)"/) do |error|
Then(/tuist generates yields error "(.+)"/) do |error|
expected_msg = error.sub!("${ARG_PATH}", @dir)
system("swift", "build")
_, _, stderr, wait_thr = Open3.popen3("swift", "run", "--skip-build", "tuist", "generate", "--path", @dir)
actual_msg = stderr.gets.to_s.strip
assert_equal(actual_msg, expected_msg)
assert_equal(wait_thr.value.exitstatus, 1)
_, stderr, status = Open3.capture3("swift", "run", "--skip-build", "tuist", "generate", "--path", @dir)
actual_msg = stderr.strip
error_message = <<~EOD
The output error message:
#{actual_msg}
Does not contain the expected:
#{error}
EOD
assert actual_msg.include?(error), error_message
refute status.success?
end
Then(/tuistenv should succeed in installing "(.+)"/) do |ref|

View File

@ -209,3 +209,7 @@ Note: to re-create `Framework2.framework` run `fixtures/ios_app_with_transitive_
## ios_app_with_pods
An iOS application with CocoaPods dependencies
## ios_app_with_incompatible_xcode
An iOS app whose TuistConfig file requires an Xcode version that is not available in the system.

View File

@ -0,0 +1,63 @@
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Xcode ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
### Xcode Patch ###
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
### Projects ###
*.xcodeproj
*.xcworkspace

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright ©. All rights reserved.</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
import ProjectDescription
let project = Project(name: "App",
targets: [
Target(name: "App",
platform: .iOS,
product: .app,
bundleId: "io.tuist.App",
infoPlist: "Info.plist",
sources: "Sources/**")
])

View File

@ -0,0 +1,20 @@
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let viewController = UIViewController()
viewController.view.backgroundColor = .white
window?.rootViewController = viewController
window?.makeKeyAndVisible()
return true
}
func hello() -> String {
return "AppDelegate.hello()"
}
}

View File

@ -0,0 +1,8 @@
import ProjectDescription
let config = TuistConfig(
compatibleXcodeVersions: ["3.2.1"],
generationOptions: [
.generateManifest
]
)