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:
parent
1aed58f4ec
commit
2b09f5cfa9
|
@ -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
|
||||
|
||||
|
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
"""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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/**")
|
||||
])
|
|
@ -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()"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import ProjectDescription
|
||||
|
||||
let config = TuistConfig(
|
||||
compatibleXcodeVersions: ["3.2.1"],
|
||||
generationOptions: [
|
||||
.generateManifest
|
||||
]
|
||||
)
|
Loading…
Reference in New Issue