diff --git a/Package.resolved b/Package.resolved index 8a451073..f73d7637 100644 --- a/Package.resolved +++ b/Package.resolved @@ -10,15 +10,6 @@ "version": "4.3.3" } }, - { - "package": "Commander", - "repositoryURL": "https://github.com/kylef/Commander.git", - "state": { - "branch": null, - "revision": "e5b50ad7b2e91eeb828393e89b03577b16be7db9", - "version": "0.8.0" - } - }, { "package": "JSONUtilities", "repositoryURL": "https://github.com/yonaskolb/JSONUtilities.git", @@ -55,6 +46,15 @@ "version": "0.9.0" } }, + { + "package": "SwiftCLI", + "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", + "state": { + "branch": null, + "revision": "37f4a7f863f6fe76ce44fc0023f331eea0089beb", + "version": "5.2.0" + } + }, { "package": "SwiftShell", "repositoryURL": "https://github.com/kareman/SwiftShell", diff --git a/Package.swift b/Package.swift index 0eb8437c..5418bcba 100644 --- a/Package.swift +++ b/Package.swift @@ -11,18 +11,23 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.0"), - .package(url: "https://github.com/kylef/Commander.git", from: "0.8.0"), .package(url: "https://github.com/jpsim/Yams.git", from: "1.0.0"), .package(url: "https://github.com/yonaskolb/JSONUtilities.git", from: "4.1.0"), .package(url: "https://github.com/kylef/Spectre.git", from: "0.9.0"), .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.0.0"), .package(url: "https://github.com/tuist/xcodeproj.git", from: "6.3.0"), + .package(url: "https://github.com/jakeheis/SwiftCLI.git", from: "5.2.0"), ], targets: [ .target(name: "XcodeGen", dependencies: [ + "XcodeGenCLI", + ]), + .target(name: "XcodeGenCLI", dependencies: [ "XcodeGenKit", - "Commander", + "ProjectSpec", + "SwiftCLI", "Rainbow", + "PathKit", ]), .target(name: "XcodeGenKit", dependencies: [ "ProjectSpec", diff --git a/Sources/XcodeGen/Logger.swift b/Sources/XcodeGen/Logger.swift deleted file mode 100644 index 21a5c655..00000000 --- a/Sources/XcodeGen/Logger.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import Rainbow - -struct Logger { - - // MARK: - Properties - - let isQuiet: Bool - let isColored: Bool - - // MARK: - Initializers - - init(isQuiet: Bool = false, isColored: Bool = true) { - self.isQuiet = isQuiet - self.isColored = isColored - } - - // MARK: - Logging - - func error(_ message: String) { - print(isColored ? message.red : message) - } - - func info(_ message: String) { - if isQuiet { - return - } - - print(message) - } - - func success(_ message: String) { - if isQuiet { - return - } - - print(isColored ? message.green : message) - } -} diff --git a/Sources/XcodeGen/main.swift b/Sources/XcodeGen/main.swift index d914b3fd..b2e1a340 100644 --- a/Sources/XcodeGen/main.swift +++ b/Sources/XcodeGen/main.swift @@ -1,89 +1,7 @@ -import Commander import Foundation -import JSONUtilities -import PathKit import ProjectSpec -import XcodeGenKit -import xcodeproj +import XcodeGenCLI let version = try Version("2.0.0") - -func generate(spec: String, project: String, isQuiet: Bool, justVersion: Bool) { - if justVersion { - print(version) - exit(EXIT_SUCCESS) - } - - let logger = Logger(isQuiet: isQuiet) - - func fatalError(_ message: String) -> Never { - logger.error(message) - exit(1) - } - - let projectSpecPath = Path(spec).absolute() - var projectPath = project == "" ? projectSpecPath.parent() : Path(project).absolute() - - if !projectSpecPath.exists { - fatalError("No project spec found at \(projectSpecPath.absolute())") - } - - let project: Project - do { - project = try Project(path: projectSpecPath) - logger.info("📋 Loaded project:\n \(project.debugDescription.replacingOccurrences(of: "\n", with: "\n "))") - } catch let error as CustomStringConvertible { - fatalError("Parsing project spec failed: \(error)") - } catch { - fatalError("Parsing project spec failed: \(error.localizedDescription)") - } - - do { - try project.validateMinimumXcodeGenVersion(version) - try project.validate() - - logger.info("⚙️ Generating project...") - let projectGenerator = ProjectGenerator(project: project) - let xcodeProject = try projectGenerator.generateXcodeProject() - - logger.info("⚙️ Writing project...") - let fileWriter = FileWriter(project: project) - projectPath = projectPath + "\(project.name).xcodeproj" - try fileWriter.writeXcodeProject(xcodeProject, to: projectPath) - try fileWriter.writePlists() - - logger.success("💾 Saved project to \(projectPath)") - } catch let error as SpecValidationError { - fatalError(error.description) - } catch { - fatalError("Generation failed: \(error.localizedDescription)") - } -} - -command( - Option( - "spec", - default: "project.yml", - flag: "s", - description: "The path to the project spec file" - ), - Option( - "project", - default: "", - flag: "p", - description: "The path to the folder where the project should be generated" - ), - Flag( - "quiet", - default: false, - flag: "q", - description: "Suppress printing of informational and success messages" - ), - Flag( - "version", - default: false, - flag: "v", - description: "Show XcodeGen version" - ), - generate -).run(version.description) +let cli = XcodeGenCLI(version: version) +cli.execute() diff --git a/Sources/XcodeGenCLI/Arguments.swift b/Sources/XcodeGenCLI/Arguments.swift new file mode 100644 index 00000000..dc995b04 --- /dev/null +++ b/Sources/XcodeGenCLI/Arguments.swift @@ -0,0 +1,10 @@ +import Foundation +import PathKit +import SwiftCLI + +extension Path: ConvertibleFromString { + + public static func convert(from: String) -> Path? { + return Path(from) + } +} diff --git a/Sources/XcodeGenCLI/CommandRouter.swift b/Sources/XcodeGenCLI/CommandRouter.swift new file mode 100644 index 00000000..e5fd3d75 --- /dev/null +++ b/Sources/XcodeGenCLI/CommandRouter.swift @@ -0,0 +1,20 @@ +import Foundation +import SwiftCLI + +class CommandRouter: Router { + + let defaultCommand: Command + + init(defaultCommand: Command) { + self.defaultCommand = defaultCommand + } + + func parse(commandGroup: CommandGroup, arguments: ArgumentList) throws -> (CommandPath, OptionRegistry) { + if !arguments.hasNext() { + arguments.manipulate { _ in + [defaultCommand.name] + } + } + return try DefaultRouter().parse(commandGroup: commandGroup, arguments: arguments) + } +} diff --git a/Sources/XcodeGenCLI/GenerateCommand.swift b/Sources/XcodeGenCLI/GenerateCommand.swift new file mode 100644 index 00000000..531f51aa --- /dev/null +++ b/Sources/XcodeGenCLI/GenerateCommand.swift @@ -0,0 +1,85 @@ +import Foundation +import SwiftCLI +import PathKit +import ProjectSpec +import XcodeGenKit +import xcodeproj + +class GenerateCommand: Command { + + let name: String = "generate" + let shortDescription: String = "Generate an Xcode project from a spec" + + let quiet = Flag("-q", "--quiet", description: "Suppress all informational and success output", defaultValue: false) + + let spec = Key("-s", "--spec", description: "The path to the project spec file. Defaults to project.yml") + + let projectDirectory = Key("-p", "--project", description: "The path to the directory where the project should be generated. Defaults to the directory the spec is in. The filename is defined in the project spec") + + let version: Version + + init(version: Version) { + self.version = version + } + + func execute() throws { + + let projectSpecPath = (spec.value ?? "project.yml").absolute() + + let projectDirectory = self.projectDirectory.value?.absolute() ?? projectSpecPath.parent() + + if !projectSpecPath.exists { + throw GenerationError.missingProjectSpec(projectSpecPath) + } + + let project: Project + do { + project = try Project(path: projectSpecPath) + } catch { + throw GenerationError.projectSpecParsingError(error) + } + + info("📋 Loaded project:\n \(project.debugDescription.replacingOccurrences(of: "\n", with: "\n "))") + + do { + try project.validateMinimumXcodeGenVersion(version) + try project.validate() + } catch let error as SpecValidationError { + throw GenerationError.validationError(error) + } + + info("⚙️ Generating project...") + let xcodeProject: XcodeProj + do { + let projectGenerator = ProjectGenerator(project: project) + xcodeProject = try projectGenerator.generateXcodeProject() + } catch { + throw GenerationError.generationError(error) + } + + info("⚙️ Writing project...") + let projectPath = projectDirectory + "\(project.name).xcodeproj" + do { + + let fileWriter = FileWriter(project: project) + try fileWriter.writeXcodeProject(xcodeProject, to: projectPath) + try fileWriter.writePlists() + } catch { + throw GenerationError.writingError(error) + } + + success("💾 Saved project to \(projectPath)") + } + + func info(_ string: String) { + if !quiet.value { + stdout.print(string) + } + } + + func success(_ string: String) { + if !quiet.value { + stdout.print(string.green) + } + } +} diff --git a/Sources/XcodeGenCLI/GenerationError.swift b/Sources/XcodeGenCLI/GenerationError.swift new file mode 100644 index 00000000..9c96ccf2 --- /dev/null +++ b/Sources/XcodeGenCLI/GenerationError.swift @@ -0,0 +1,36 @@ +import PathKit +import Foundation +import ProjectSpec +import SwiftCLI +import Rainbow + +enum GenerationError: Error, CustomStringConvertible, ProcessError { + case missingProjectSpec(Path) + case projectSpecParsingError(Error) + case validationError(SpecValidationError) + case generationError(Error) + case writingError(Error) + + var description: String { + switch self { + case .missingProjectSpec(let path): + return "No project spec found at \(path.absolute())" + case .projectSpecParsingError(let error): + return "Parsing project spec failed: \(error)" + case .validationError(let error): + return error.description + case .generationError(let error): + return String(describing: error) + case .writingError(let error): + return String(describing: error) + } + } + + var message: String? { + return description.red + } + + var exitStatus: Int32 { + return 1 + } +} diff --git a/Sources/XcodeGenCLI/XcodeGenCLI.swift b/Sources/XcodeGenCLI/XcodeGenCLI.swift new file mode 100644 index 00000000..03c6a7d1 --- /dev/null +++ b/Sources/XcodeGenCLI/XcodeGenCLI.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftCLI +import ProjectSpec + +public class XcodeGenCLI { + + let cli: CLI + + public init(version: Version) { + let generateCommand = GenerateCommand(version: version) + + cli = CLI(name: "xcodegen", + version: version.string, + description: "Generates Xcode projects", + commands: [generateCommand]) + cli.parser = Parser(router: CommandRouter(defaultCommand: generateCommand)) + } + + public func execute(arguments: [String]? = nil) { + let status: Int32 + if let arguments = arguments { + status = cli.go(with: arguments) + } else { + status = cli.go() + } + exit(status) + } +}