diff --git a/Sources/CreateProject/Color.swift b/Sources/CreateProject/Color.swift index 44b1ed7..5516bfa 100644 --- a/Sources/CreateProject/Color.swift +++ b/Sources/CreateProject/Color.swift @@ -1,3 +1,5 @@ +import class Foundation.ProcessInfo + enum Color: String { case reset = "\u{001B}[0;0m" case black = "\u{001B}[0;30m" @@ -19,6 +21,9 @@ enum Color: String { case bgWhite = "\u{001B}[0;47m" static func +(color: Color, text: String) -> String { + guard UserChoice.shouldColorizeOutput else { + return text + } return color.rawValue + text + Color.reset.rawValue } } diff --git a/Sources/CreateProject/CreateProject.swift b/Sources/CreateProject/CreateProject.swift index 76b4d25..fd0beff 100644 --- a/Sources/CreateProject/CreateProject.swift +++ b/Sources/CreateProject/CreateProject.swift @@ -1,12 +1,15 @@ import Foundation +let version = "2.2.0" + @main private struct CreateProject { - static let fileManager = FileManager() - static func main() throws { + checkHelp() + checkShowVersion() + do { - let projectPath: String = try UserChoice.get(message: "Please enter where you would like the project directory to be created: ") + let projectPath = try getProjectPath() let (projectPathExists, projectPathIsDirectory) = fileManager.directoryExists(atPath: projectPath) @@ -23,14 +26,15 @@ private struct CreateProject { throw ChangeDirectoryError(path: projectPath) } - let projectName: String = try UserChoice.get(message: "Please enter the name of the project: ") + let projectName = try getProjectName() + let contents = try fileManager.contentsOfDirectory(atPath: fileManager.currentDirectoryPath) guard contents.isEmpty else { print(color: .red, "Directory at \(projectPath) must be empty, but found: \(contents.joined(separator: ", "))") exit(1) } - let executableName: String = try UserChoice.get(message: "Please enter the name of the executable: ") + let executableName = try getExecutableName() guard try UserChoice.getBool(message: "Project will be created at: \(fileManager.currentDirectoryPath + "/Package.swift, would you like to proceed?")") else { do { @@ -43,10 +47,15 @@ private struct CreateProject { exit(0) } - let godotPath: String = switch ProcessInfo.processInfo.environment["GODOT"] { - case .some(let value) where value.isEmpty == false: value - default: try UserChoice.get(message: Color.yellow + "GODOT not set\n" + "Please enter the full path to the Godot 4.2 executable: ") + let godotPath = try getGodotPath() + + #if os(macOS) + guard !godotPath.hasSuffix(".app") else { + print(color: .red, "GODOT path ends in .app, it should be the path to executable itself") + print(color: .yellow, "try \(godotPath)/Contents/MacOS/Godot") + exit(1) } + #endif print(color: .green, "Created \(try FileFactory.createPackageFile(projectName: projectName, executableName: executableName))") print(color: .green, "Created \(try FileFactory.copyReadmeFile())") @@ -78,6 +87,48 @@ private struct CreateProject { cd \(fileManager.currentDirectoryPath) && make all """) } + + static let fileManager = FileManager() +} + +private func checkHelp() { + guard !UserChoice.shouldHelp else { + print(UserChoice.Argument.allCases.map { $0.usage + "\n\t" + $0.description }.joined(separator: "\n")) + exit(0) + } +} + +private func checkShowVersion() { + guard !UserChoice.shouldShowVersion else { + print(version) + exit(0) + } +} + +private func getProjectPath() throws -> String { + try checkFor(.projectPath, promptIfNeeded: "Please enter where you would like the project directory to be created: ") +} + +private func getProjectName() throws -> String { + try checkFor(.projectName, promptIfNeeded: "Please enter the name of the project: ") +} + +private func getExecutableName() throws -> String { + try checkFor(.executableName, promptIfNeeded: "Please enter the name of the executable: ") +} + +private func getGodotPath() throws -> String { + switch ProcessInfo.processInfo.environment["GODOT"] { + case .some(let value) where value.isEmpty == false: value + default: try UserChoice.get(message: Color.yellow + "GODOT not set\n" + "Please enter the full path to the Godot 4.2 executable: ") + } +} + +private func checkFor(_ argument: UserChoice.Argument, promptIfNeeded prompt: String) throws -> String { + switch ProcessInfo.processInfo.string(for: argument) { + case .some(let argument): argument + case .none: try UserChoice.get(message: prompt) + } } func print(color: Color, _ message: String) { diff --git a/Sources/CreateProject/DataFactory.swift b/Sources/CreateProject/DataFactory.swift index 2fed0c3..b462a15 100644 --- a/Sources/CreateProject/DataFactory.swift +++ b/Sources/CreateProject/DataFactory.swift @@ -165,22 +165,22 @@ enum DataFactory { compatibility_minimum = 4.2 [libraries] - macos.debug = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - macos.release = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - windows.debug.x86_32 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - windows.release.x86_32 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - windows.debug.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - windows.release.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.debug.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.release.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.debug.arm64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.release.arm64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.debug.rv64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - linux.release.rv64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - android.debug.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - android.release.x86_64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - android.debug.arm64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" - android.release.arm64 = "res://bin/lib\(projectName).\(arch.dynamicExtension)" + macos.debug = "res://bin/lib\(projectName).\(dynamicExtension)" + macos.release = "res://bin/lib\(projectName).\(dynamicExtension)" + windows.debug.x86_32 = "res://bin/lib\(projectName).\(dynamicExtension)" + windows.release.x86_32 = "res://bin/lib\(projectName).\(dynamicExtension)" + windows.debug.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + windows.release.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.debug.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.release.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.debug.arm64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.release.arm64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.debug.rv64 = "res://bin/lib\(projectName).\(dynamicExtension)" + linux.release.rv64 = "res://bin/lib\(projectName).\(dynamicExtension)" + android.debug.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + android.release.x86_64 = "res://bin/lib\(projectName).\(dynamicExtension)" + android.debug.arm64 = "res://bin/lib\(projectName).\(dynamicExtension)" + android.release.arm64 = "res://bin/lib\(projectName).\(dynamicExtension)" """ .utf8Data @@ -259,8 +259,8 @@ enum DataFactory { rm -rf $(GODOT_BIN_PATH) mkdir -p $(GODOT_BIN_PATH) - cp $(BUILD_PATH)/debug/libSwiftGodot.\(arch.dynamicExtension) $(GODOT_BIN_PATH) - cp $(BUILD_PATH)/debug/lib\(projectName).\(arch.dynamicExtension) $(GODOT_BIN_PATH) + cp $(BUILD_PATH)/debug/libSwiftGodot.\(dynamicExtension) $(GODOT_BIN_PATH) + cp $(BUILD_PATH)/debug/lib\(projectName).\(dynamicExtension) $(GODOT_BIN_PATH) .PHONY: run run: @@ -282,28 +282,13 @@ enum DataFactory { } } -private enum Architecture { - case x86_64 - case arm64 - - var dynamicExtension: String { - switch self { - case .x86_64: "so" - case .arm64: "dylib" - } - } -} - -private extension DataFactory { - static var arch: Architecture { - #if arch(x86_64) - .x86_64 - #elseif arch(arm64) - .arm64 - #else - fatalError("Unknown architecture") - #endif - } +// on macOS, dynamic libraries have the .dylib file extension, but on other platforms it's .so +private var dynamicExtension: String { + #if os(macOS) + "dylib" + #else + "so" + #endif } private extension String { diff --git a/Sources/CreateProject/UserChoice.swift b/Sources/CreateProject/UserChoice.swift index 8566c44..55635ae 100644 --- a/Sources/CreateProject/UserChoice.swift +++ b/Sources/CreateProject/UserChoice.swift @@ -1,5 +1,35 @@ import Foundation +extension ProcessInfo { + func string(for argument: UserChoice.Argument) -> String? { + let arguments = ProcessInfo.processInfo.arguments + + guard let firstIndex = arguments.firstIndex(of: argument.rawValue) else { + return nil + } + + guard arguments.indices.contains(firstIndex + 1) else { + return nil + } + + return arguments[firstIndex + 1] + } + + func bool(for argument: UserChoice.Argument) -> Bool? { + let arguments = ProcessInfo.processInfo.arguments + + guard let firstIndex = arguments.firstIndex(of: argument.rawValue) else { + return nil + } + + guard arguments.indices.contains(firstIndex + 1) else { + return nil + } + + return Bool(arguments[firstIndex + 1]) + } +} + enum UserChoice { enum Error: Swift.Error, LocalizedError { case missingUserChoice @@ -12,6 +42,50 @@ enum UserChoice { } } } + + enum Argument: String, CaseIterable { + case help = "--help" + case noColor = "--noColor" + case projectName = "--projectName" + case executableName = "--executableName" + case projectPath = "--projectPath" + case godotPath = "--godot" + case version = "--version" + + var description: String { + switch self { + case .help: "show help info" + case .noColor: "disable colorized output" + case .version: "show current version" + case .projectName: "project name" + case .executableName: "executable name" + case .godotPath: "path to Godot binary" + case .projectPath: "path where Package.swift and other files will be saved" + } + } + + var usage: String { + switch self { + case .help, .noColor, .version: + rawValue + + case .projectName, .executableName: rawValue + " " + case .projectPath, .godotPath: rawValue + " " + } + } + } + + static var shouldColorizeOutput: Bool = { + !ProcessInfo.processInfo.arguments.contains(Argument.noColor.rawValue) + }() + + static var shouldHelp: Bool = { + ProcessInfo.processInfo.arguments.contains(Argument.help.rawValue) + }() + + static var shouldShowVersion: Bool = { + ProcessInfo.processInfo.arguments.contains(Argument.version.rawValue) + }() static func get(message: String) throws -> String { print(message, terminator: "")