From e516fe20a6da817f56221424a71e4730bae18d1d Mon Sep 17 00:00:00 2001 From: Geoffrey Foster Date: Mon, 10 Dec 2018 21:56:18 -0500 Subject: [PATCH] renaming to xcoder making build/release better splitting the sort into two different commands --- Makefile | 37 ++- Package.resolved | 17 +- Package.swift | 34 +-- README.md | 2 +- Sources/Bullwinkle/main.swift | 9 - Sources/MooseKit/Bullwinkle.swift | 77 ------- Sources/MooseKit/Command.swift | 16 -- Sources/MooseKit/CompletionToolCommand.swift | 35 --- Sources/MooseKit/SortCommand.swift | 215 ------------------ .../ArgumentKindExtensions.swift | 2 +- .../Commands/BuildPhaseSortCommand.swift | 127 +++++++++++ .../Commands/CompletionToolCommand.swift | 35 +++ .../XcoderKit/Commands/GroupSortCommand.swift | 45 ++++ .../Commands}/SyncCommand.swift | 43 ++-- Sources/XcoderKit/Error.swift | 29 +++ .../PBXObject+Extensions.swift | 8 +- Sources/XcoderKit/XcodeProjectLoading.swift | 52 +++++ Sources/XcoderKit/XcodeReferenceSorting.swift | 38 ++++ Sources/xcoder/Version.swift | 5 + Sources/xcoder/main.swift | 11 + Version.swift.template | 5 + 21 files changed, 429 insertions(+), 413 deletions(-) delete mode 100644 Sources/Bullwinkle/main.swift delete mode 100644 Sources/MooseKit/Bullwinkle.swift delete mode 100644 Sources/MooseKit/Command.swift delete mode 100644 Sources/MooseKit/CompletionToolCommand.swift delete mode 100644 Sources/MooseKit/SortCommand.swift rename Sources/{MooseKit => XcoderKit}/ArgumentKindExtensions.swift (95%) create mode 100644 Sources/XcoderKit/Commands/BuildPhaseSortCommand.swift create mode 100644 Sources/XcoderKit/Commands/CompletionToolCommand.swift create mode 100644 Sources/XcoderKit/Commands/GroupSortCommand.swift rename Sources/{MooseKit => XcoderKit/Commands}/SyncCommand.swift (55%) create mode 100644 Sources/XcoderKit/Error.swift rename Sources/{MooseKit => XcoderKit}/PBXObject+Extensions.swift (90%) create mode 100644 Sources/XcoderKit/XcodeProjectLoading.swift create mode 100644 Sources/XcoderKit/XcodeReferenceSorting.swift create mode 100644 Sources/xcoder/Version.swift create mode 100644 Sources/xcoder/main.swift create mode 100644 Version.swift.template diff --git a/Makefile b/Makefile index 2a4da0f..7e8d892 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,42 @@ -SWIFTC_FLAGS = -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.12" -CONFIGURATION = release +TOOL_NAME = xcoder +VERSION = 0.3.0 + +REPO = https://github.com/g-Off/$(TOOL_NAME) +RELEASE_TAR = $(REPO)/archive/$(VERSION).tar.gz +SHA = $(shell curl -L -s $(RELEASE_TAR) | shasum -a 256 | sed 's/ .*//') -all: build +PREFIX = /usr/local +INSTALL_PATH = $(PREFIX)/bin/$(TOOL_NAME) +BUILD_PATH = $(shell swift build --show-bin-path -c $(CONFIGURATION))/$(TOOL_NAME) + +SWIFTC_FLAGS = -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.12" +CONFIGURATION = debug -debug: CONFIGURATION = debug +debug: generate_version debug: build + +generate_version: + @sed 's/__VERSION__/$(VERSION)/g' Version.swift.template > Sources/xcoder/Version.swift + +release_sha: + @echo $(SHA) build: swift build --configuration $(CONFIGURATION) $(SWIFTC_FLAGS) -release: CONFIGURATION = release -release: - swift build --configuration $(CONFIGURATION) $(SWIFTC_FLAGS) +install: CONFIGURATION = release +install: SWIFTC_FLAGS += --static-swift-stdlib --disable-sandbox +install: clean build + mkdir -p $(PREFIX)/bin + cp -f $(BUILD_PATH) $(INSTALL_PATH) test: swift test $(SWIFTC_FLAGS) +xcode: generate_version xcode: - swift package generate-xcodeproj --xcconfig-overrides=overrides.xcconfig + swift package generate-xcodeproj --xcconfig-overrides=Overrides.xcconfig + xed . clean: - swift package clean \ No newline at end of file + swift package clean diff --git a/Package.resolved b/Package.resolved index b3d2b95..3aad30a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,22 @@ { "object": { "pins": [ + { + "package": "CommandRegistry", + "repositoryURL": "https://github.com/g-Off/CommandRegistry.git", + "state": { + "branch": "master", + "revision": "142aa27445e7998c5201b5ec9682698195d6701a", + "version": null + } + }, { "package": "SwiftPM", "repositoryURL": "https://github.com/apple/swift-package-manager.git", "state": { "branch": null, - "revision": "6983434787dec4e543e9d398a0a9acf63ccd4da1", - "version": "0.2.1" + "revision": "235aacc514cb81a6881364b0fedcb3dd083228f3", + "version": "0.3.0" } }, { @@ -15,8 +24,8 @@ "repositoryURL": "https://github.com/g-Off/XcodeProject.git", "state": { "branch": null, - "revision": "c4215ece309e60d646e510786fe0f60960419837", - "version": "0.3.1" + "revision": "a1e12a8659684039258b79761c65abb9ec98fc94", + "version": "0.4.0" } } ] diff --git a/Package.swift b/Package.swift index 85ebd71..8fd3795 100644 --- a/Package.swift +++ b/Package.swift @@ -1,42 +1,32 @@ -// swift-tools-version:4.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:4.2 import PackageDescription let package = Package( - name: "Bullwinkle", + name: "Xcoder", products: [ .executable( - name: "bullwinkle", - targets: ["bullwinkle"]), + name: "xcoder", + targets: ["xcoder"]), .library( - name: "MooseKit", - targets: ["MooseKit"]), + name: "XcoderKit", + targets: ["XcoderKit"]), ], dependencies: [ - .package( - url: "https://github.com/g-Off/XcodeProject.git", - from: "0.3.1" - ), - .package( - url: "https://github.com/apple/swift-package-manager.git", - from: "0.1.0" - ) + .package(url: "https://github.com/g-Off/XcodeProject.git", from: "0.4.0"), + .package(url: "https://github.com/g-Off/CommandRegistry.git", .branch("master")) ], targets: [ .target( - name: "bullwinkle", + name: "xcoder", dependencies: [ - "XcodeProject", - "Utility", - "MooseKit" + "XcoderKit" ] ), .target( - name: "MooseKit", + name: "XcoderKit", dependencies: [ "XcodeProject", - "Utility" + "CommandRegistry" ] ) ] diff --git a/README.md b/README.md index 5e817e1..d4e41fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Bullwinkle +# xcoder A simple command line tool for sorting and syncing an Xcode project file. diff --git a/Sources/Bullwinkle/main.swift b/Sources/Bullwinkle/main.swift deleted file mode 100644 index 690ef64..0000000 --- a/Sources/Bullwinkle/main.swift +++ /dev/null @@ -1,9 +0,0 @@ -import MooseKit -import Foundation - -do { - let command = URL(fileURLWithPath: CommandLine.arguments[0]).lastPathComponent - try Bullwinkle.execute(command: command, arguments: Array(CommandLine.arguments.dropFirst())) -} catch let error { - print(error) -} diff --git a/Sources/MooseKit/Bullwinkle.swift b/Sources/MooseKit/Bullwinkle.swift deleted file mode 100644 index 401bd76..0000000 --- a/Sources/MooseKit/Bullwinkle.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// Bullwinkle.swift -// bullwinkle -// -// Created by Geoffrey Foster on 2018-01-13. -// - -import Foundation -import Utility -import Basic - -public final class Bullwinkle { - public static let version = Version(0, 1, 0) - enum Error: Swift.Error, CustomStringConvertible { - case invalidProject(path: String?) - case invalidGroup(String) - case invalidTarget(String) - - var description: String { - switch self { - case .invalidProject(let path): - if let path = path { - return "Invalid project at \(path)" - } else { - return "No project specified" - } - case .invalidGroup(let groupPath): - return "Invalid group path \(groupPath)" - case .invalidTarget(let targetName): - return "Invalid target \(targetName)" - } - } - } - - struct Options { - public var shouldPrintVersion: Bool = false - } - - public static func execute(command: String, arguments: [String]) throws { - let parser = ArgumentParser(commandName: command, usage: "[subcommand]", overview: "sort and sync Xcode project groups") - - let binder = ArgumentBinder() - binder.bind(option: parser.add(option: "--version", shortName: "-v", kind: Bool.self, usage: "Prints the current version of \(command.capitalized)")) { (options, shouldPrintVersion) in - options.shouldPrintVersion = shouldPrintVersion - } - - let commands: [Command] = [ - SortCommand(parser: parser), - SyncCommand(parser: parser), - CompletionToolCommand(parser: parser) - ] - - let parsedArguments = try parser.parse(arguments) - - var options = Options() - binder.fill(parsedArguments, into: &options) - - if options.shouldPrintVersion { - print(Bullwinkle.version) - exit(0) - } - - if let subParser = parsedArguments.subparser(parser), - let command = commands.first(where : { $0.name == subParser }) { - do { - try command.execute(parsedArguments: parsedArguments) - } catch ArgumentParserError.expectedValue(let value) { - print("Missing value for argument \(value).") - } catch ArgumentParserError.expectedArguments(let parser, let arguments) { - print("Missing arguments: \(arguments.joined()).") - parser.printUsage(on: stdoutStream) - } - } else { - parser.printUsage(on: stdoutStream) - } - } -} diff --git a/Sources/MooseKit/Command.swift b/Sources/MooseKit/Command.swift deleted file mode 100644 index 58fffbb..0000000 --- a/Sources/MooseKit/Command.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Command.swift -// bullwinkle -// -// Created by Geoffrey Foster on 2018-01-10. -// - -import Utility -import Foundation -import XcodeProject - -protocol Command { - var name: String { get } - - func execute(parsedArguments: ArgumentParser.Result) throws -} diff --git a/Sources/MooseKit/CompletionToolCommand.swift b/Sources/MooseKit/CompletionToolCommand.swift deleted file mode 100644 index 2f695b7..0000000 --- a/Sources/MooseKit/CompletionToolCommand.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// CompletionToolCommand.swift -// bullwinkle -// -// Created by Geoffrey Foster on 2018-01-13. -// - -import Foundation -import Utility -import Basic - -class CompletionToolCommand: Command { - struct Options { - var completionToolMode: Shell = .bash - } - let name: String = "completion-tool" - let binder = ArgumentBinder() - let parser: ArgumentParser - - required init(parser: ArgumentParser) { - self.parser = parser - let completionToolParser = parser.add(subparser: name, overview: "Completion tool (for shell completions)") - let f = completionToolParser.add(positional: "mode", kind: Shell.self) - binder.bind(positional: f) { (options, mode) in - options.completionToolMode = mode - } - } - - func execute(parsedArguments: ArgumentParser.Result) throws { - var options = Options() - binder.fill(parsedArguments, into: &options) - - parser.generateCompletionScript(for: options.completionToolMode, on: stdoutStream) - } -} diff --git a/Sources/MooseKit/SortCommand.swift b/Sources/MooseKit/SortCommand.swift deleted file mode 100644 index b5d476e..0000000 --- a/Sources/MooseKit/SortCommand.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// SortCommand.swift -// bullwinkle -// -// Created by Geoffrey Foster on 2018-01-10. -// - -import Foundation -import Utility -import XcodeProject -import Basic - -extension PBXGroup.SortOption: StringEnumArgument { - static var options: [(name: String, description: String)] = [ - (name: PBXGroup.SortOption.name.rawValue, description: "Sort by name"), - (name: PBXGroup.SortOption.type.rawValue, description: "Sort by type") - ] - public static var completion: ShellCompletion { - return .values(options.map { (value: $0.name, description: $0.description) }) - } -} - -final class SortArguments { - enum BuildPhase: String { - case frameworks - case headers - case resources - case sources - - func matches(buildPhase: PBXBuildPhase) -> Bool { - switch self { - case .frameworks where buildPhase is PBXFrameworksBuildPhase: - return true - case .headers where buildPhase is PBXHeadersBuildPhase: - return true - case .resources where buildPhase is PBXResourcesBuildPhase: - return true - case .sources where buildPhase is PBXSourcesBuildPhase: - return true - default: - return false - } - } - } - - var xcodeproj: Foundation.URL? - var order: PBXGroup.SortOption = .name - - var group: String? - var recursive: Bool = false - - var target: String? - var phase: BuildPhase = .sources - var overrides: [String] = [] -} - -extension SortArguments.BuildPhase: StringEnumArgument { - static var completion: ShellCompletion { - return .values( - [ - (value: SortArguments.BuildPhase.frameworks.rawValue, description: "Frameworks build phase"), - (value: SortArguments.BuildPhase.headers.rawValue, description: "Headers build phase"), - (value: SortArguments.BuildPhase.resources.rawValue, description: "Resources build phase"), - (value: SortArguments.BuildPhase.sources.rawValue, description: "Sources build phase") - ] - ) - } -} - -final class SortCommand: Command { - private static func projectLoaded(from arguments: SortArguments) throws -> ProjectFile { - func projectURL(from arguments: SortArguments, currentWorkingDirectory: AbsolutePath = currentWorkingDirectory) throws -> Foundation.URL { - if let xcodeproj = arguments.xcodeproj { - return xcodeproj - } - let dirs = try localFileSystem.getDirectoryContents(currentWorkingDirectory) - if let xcodeproj = dirs.first(where: { $0.hasPrefix("xcodeproj") }) { - return URL(fileURLWithPath: xcodeproj, relativeTo: URL(fileURLWithPath: currentWorkingDirectory.asString, isDirectory: true)) - } - throw Bullwinkle.Error.invalidProject(path: nil) - } - let xcodeproj = try projectURL(from: arguments) - let projectFile: ProjectFile - do { - projectFile = try ProjectFile(url: xcodeproj) - } catch { - throw Bullwinkle.Error.invalidProject(path: xcodeproj.path) - } - return projectFile - } - - final class Group: Command { - let name: String = "group" - let binder: ArgumentBinder - - required init(parser: ArgumentParser, binder: ArgumentBinder) { - self.binder = binder - let subparser = parser.add(subparser: name, overview: "Sorts the given group (or top-level)") - binder.bind(positional: subparser.add(positional: "group", kind: String.self, optional: true, usage: "Group name to sort. For a nested group specify the full path from parent to child with / inbetween.")) { (sortArguments, group) in - sortArguments.group = group - } - binder.bind(option: subparser.add(option: "--recursive", shortName: "-r", kind: Bool.self, usage: "Recursively descend through all child groups.")) { (sortArguments, recursive) in - sortArguments.recursive = recursive - } - } - - func execute(parsedArguments: ArgumentParser.Result) throws { - var arguments = SortArguments() - binder.fill(parsedArguments, into: &arguments) - let projectFile = try SortCommand.projectLoaded(from: arguments) - let group = try projectFile.group(forPath: arguments.group) - group.sort(recursive: arguments.recursive, by: arguments.order) - try projectFile.save() - } - } - final class BuildPhase: Command { - let name: String = "phase" - let binder: ArgumentBinder - - required init(parser: ArgumentParser, binder: ArgumentBinder) { - self.binder = binder - let subparser = parser.add(subparser: name, overview: "Sorts the given phase (or top-level)") - binder.bind(positional: subparser.add(positional: "phase", kind: SortArguments.BuildPhase.self, optional: true, usage: "Name of the build phase to sort.")) { (sortArguments, phase) in - sortArguments.phase = phase - } - binder.bind(option: subparser.add(option: "--target", shortName: "-t", kind: String.self, usage: "Name of the target.")) { (sortArguments, target) in - sortArguments.target = target - } - binder.bindArray(option: subparser.add(option: "--override-group", kind: [String].self, strategy: .upToNextOption)) { (sortArguments, overrides) in - sortArguments.overrides = overrides - } - } - - func execute(parsedArguments: ArgumentParser.Result) throws { - func findTarget(from projectFile: ProjectFile, named targetName: String?) throws -> PBXTarget { - let foundTarget: PBXTarget? - if let targetName = targetName { - foundTarget = projectFile.project.targets.first { $0.name == targetName } - } else { - foundTarget = projectFile.project.targets.first - } - guard let target = foundTarget else { - throw Bullwinkle.Error.invalidTarget(targetName ?? "No target specified") - } - return target - } - - func sort(phase: PBXBuildPhase, order: PBXReference.SortOption, overrides: [PBXGroup]) { - phase.sort(by: order) - - var indices: [Int] = [] - for index in phase.files.indices { - let buildFile = phase.files[index] - guard let fileRef = buildFile.fileRef else { continue } - if let _ = overrides.first(where: { $0.contains(fileRef, recursive: true) }) { - indices.append(index) - } - } - - let files = indices.map { phase.files[$0] } - for index in indices.lazy.reversed() { - phase.remove(at: index) - } - phase.insert(contentsOf: files, at: 0) - } - var arguments = SortArguments() - binder.fill(parsedArguments, into: &arguments) - let projectFile = try SortCommand.projectLoaded(from: arguments) - - let overrideGroups = try arguments.overrides.flatMap { try projectFile.group(forPath: $0) } - let target = try findTarget(from: projectFile, named: arguments.target) - if let phase = target.buildPhases.first(where: { arguments.phase.matches(buildPhase: $0) }) { - sort(phase: phase, order: arguments.order, overrides: overrideGroups) - } else { - target.buildPhases.forEach { phase in - guard SortArguments.BuildPhase.frameworks.matches(buildPhase: phase) || - SortArguments.BuildPhase.headers.matches(buildPhase: phase) || - SortArguments.BuildPhase.resources.matches(buildPhase: phase) || - SortArguments.BuildPhase.sources.matches(buildPhase: phase) else { - return - } - sort(phase: phase, order: arguments.order, overrides: overrideGroups) - } - } - try projectFile.save() - } - } - let name: String = "sort" - let binder = ArgumentBinder() - let subparser: ArgumentParser - let commands: [Command] - - required init(parser: ArgumentParser) { - subparser = parser.add(subparser: name, overview: "Sorts the given group (or top-level)") - commands = [ - Group(parser: subparser, binder: binder), - BuildPhase(parser: subparser, binder: binder) - ] - binder.bind(option: subparser.add(option: "--xcodeproj", kind: PathArgument.self, usage: "Xcode Project file.")) { (sortArguments, xcodeproj) in - sortArguments.xcodeproj = URL(fileURLWithPath: xcodeproj.path.asString) - } - binder.bind(option: subparser.add(option: "--order", shortName: "-o", kind: PBXGroup.SortOption.self, usage: "Sort the group by one of [\(PBXGroup.SortOption.options.map { $0.name }.joined(separator: ", "))]")) { (sortArguments, order) in - sortArguments.order = order - } - } - - func execute(parsedArguments: ArgumentParser.Result) throws { - if let commandName = parsedArguments.subparser(subparser), - let command = commands.first(where: { $0.name == commandName }) { - try command.execute(parsedArguments: parsedArguments) - } else { - subparser.printUsage(on: stdoutStream) - } - } -} diff --git a/Sources/MooseKit/ArgumentKindExtensions.swift b/Sources/XcoderKit/ArgumentKindExtensions.swift similarity index 95% rename from Sources/MooseKit/ArgumentKindExtensions.swift rename to Sources/XcoderKit/ArgumentKindExtensions.swift index 932367a..a88dcfd 100644 --- a/Sources/MooseKit/ArgumentKindExtensions.swift +++ b/Sources/XcoderKit/ArgumentKindExtensions.swift @@ -1,6 +1,6 @@ // // ArgumentKindExtensions.swift -// bullwinkle +// xcoder // // Created by Geoffrey Foster on 2018-01-11. // diff --git a/Sources/XcoderKit/Commands/BuildPhaseSortCommand.swift b/Sources/XcoderKit/Commands/BuildPhaseSortCommand.swift new file mode 100644 index 0000000..114d9e6 --- /dev/null +++ b/Sources/XcoderKit/Commands/BuildPhaseSortCommand.swift @@ -0,0 +1,127 @@ +// +// BuildPhaseSortCommand.swift +// XcoderKit +// +// Created by Geoffrey Foster on 2018-12-09. +// + +import Foundation +import Utility +import XcodeProject +import Basic +import CommandRegistry + +public struct BuildPhaseSortCommand: Command { + private struct SortArguments: XcodeProjectLoading, XcodeReferenceSorting { + enum BuildPhase: String, StringEnumArgument { + case frameworks + case headers + case resources + case sources + + func matches(buildPhase: PBXBuildPhase) -> Bool { + switch self { + case .frameworks where buildPhase is PBXFrameworksBuildPhase: + return true + case .headers where buildPhase is PBXHeadersBuildPhase: + return true + case .resources where buildPhase is PBXResourcesBuildPhase: + return true + case .sources where buildPhase is PBXSourcesBuildPhase: + return true + default: + return false + } + } + + static var completion: ShellCompletion { + return .values( + [ + (value: SortArguments.BuildPhase.frameworks.rawValue, description: "Frameworks build phase"), + (value: SortArguments.BuildPhase.headers.rawValue, description: "Headers build phase"), + (value: SortArguments.BuildPhase.resources.rawValue, description: "Resources build phase"), + (value: SortArguments.BuildPhase.sources.rawValue, description: "Sources build phase") + ] + ) + } + } + + var xcodeproj: AbsolutePath = localFileSystem.currentWorkingDirectory! + var order: PBXReference.SortOption = .name + var target: String? + var phase: BuildPhase = .sources + var overrides: [String] = [] + } + public let command: String = "phase-sort" + public let overview: String = "Sorts the given phase (or top-level)" + private let binder = ArgumentBinder() + + public init(parser: ArgumentParser) { + let subparser = parser.add(subparser: command, overview: overview) + binder.bind(positional: subparser.add(positional: "phase", kind: SortArguments.BuildPhase.self, optional: true, usage: "Name of the build phase to sort.")) { (sortArguments, phase) in + sortArguments.phase = phase + } + binder.bindXcodeProject(parser: subparser) + binder.bindReferenceSorting(parser: subparser) + binder.bind(option: subparser.add(option: "--target", shortName: "-t", kind: String.self, usage: "Name of the target.")) { (sortArguments, target) in + sortArguments.target = target + } + binder.bindArray(option: subparser.add(option: "--override-group", kind: [String].self, strategy: .upToNextOption)) { (sortArguments, overrides) in + sortArguments.overrides = overrides + } + } + + public func run(with arguments: ArgumentParser.Result) throws { + func findTarget(from projectFile: ProjectFile, named targetName: String?) throws -> PBXTarget { + let foundTarget: PBXTarget? + if let targetName = targetName { + foundTarget = projectFile.project.targets.first { $0.name == targetName } + } else { + foundTarget = projectFile.project.targets.first + } + guard let target = foundTarget else { + throw Error.invalidTarget(targetName ?? "No target specified") + } + return target + } + + func sort(phase: PBXBuildPhase, order: PBXReference.SortOption, overrides: [PBXGroup]) { + phase.sort(by: order) + + var indices: [Int] = [] + for index in phase.files.indices { + let buildFile = phase.files[index] + guard let fileRef = buildFile.fileRef else { continue } + if let _ = overrides.first(where: { $0.contains(fileRef, recursive: true) }) { + indices.append(index) + } + } + + let files = indices.map { phase.files[$0] } + for index in indices.lazy.reversed() { + phase.remove(at: index) + } + phase.insert(contentsOf: files, at: 0) + } + var sortArguments = SortArguments() + try binder.fill(parseResult: arguments, into: &sortArguments) + let projectFile = try sortArguments.loadedProject() + + let overrideGroups = try sortArguments.overrides.compactMap { try projectFile.group(forPath: $0) } + let target = try findTarget(from: projectFile, named: sortArguments.target) + if let phase = target.buildPhases.first(where: { sortArguments.phase.matches(buildPhase: $0) }) { + sort(phase: phase, order: sortArguments.order, overrides: overrideGroups) + } else { + target.buildPhases.forEach { phase in + guard SortArguments.BuildPhase.frameworks.matches(buildPhase: phase) || + SortArguments.BuildPhase.headers.matches(buildPhase: phase) || + SortArguments.BuildPhase.resources.matches(buildPhase: phase) || + SortArguments.BuildPhase.sources.matches(buildPhase: phase) else { + return + } + sort(phase: phase, order: sortArguments.order, overrides: overrideGroups) + } + } + try projectFile.save() + } +} diff --git a/Sources/XcoderKit/Commands/CompletionToolCommand.swift b/Sources/XcoderKit/Commands/CompletionToolCommand.swift new file mode 100644 index 0000000..881e800 --- /dev/null +++ b/Sources/XcoderKit/Commands/CompletionToolCommand.swift @@ -0,0 +1,35 @@ +// +// CompletionToolCommand.swift +// xcoder +// +// Created by Geoffrey Foster on 2018-01-13. +// + +import Foundation +import Utility +import Basic +import CommandRegistry + +public struct CompletionToolCommand: Command { + private struct Options { + var completionToolMode: Shell = .bash + } + public let command: String = "completion-tool" + public let overview: String = "Completion tool (for shell completions)" + private let binder = ArgumentBinder() + private let parser: ArgumentParser + + public init(parser: ArgumentParser) { + self.parser = parser + let subparser = parser.add(subparser: command, overview: overview) + binder.bind(positional: subparser.add(positional: "mode", kind: Shell.self)) { (options, mode) in + options.completionToolMode = mode + } + } + + public func run(with arguments: ArgumentParser.Result) throws { + var options = Options() + try binder.fill(parseResult: arguments, into: &options) + parser.generateCompletionScript(for: options.completionToolMode, on: stdoutStream) + } +} diff --git a/Sources/XcoderKit/Commands/GroupSortCommand.swift b/Sources/XcoderKit/Commands/GroupSortCommand.swift new file mode 100644 index 0000000..b9333e5 --- /dev/null +++ b/Sources/XcoderKit/Commands/GroupSortCommand.swift @@ -0,0 +1,45 @@ +// +// GroupSortCommand.swift +// XcoderKit +// +// Created by Geoffrey Foster on 2018-12-09. +// + +import Foundation +import Utility +import XcodeProject +import Basic +import CommandRegistry + +public struct GroupSortCommand: Command { + private struct SortArguments: XcodeProjectLoading, XcodeReferenceSorting { + var xcodeproj: AbsolutePath = localFileSystem.currentWorkingDirectory! + var order: PBXReference.SortOption = .name + var group: String? + var recursive: Bool = false + } + public let command: String = "group-sort" + public let overview: String = "Sorts the given group (or top-level)" + private let binder = ArgumentBinder() + + public init(parser: ArgumentParser) { + let subparser = parser.add(subparser: command, overview: "Sorts the given group (or top-level)") + binder.bind(positional: subparser.add(positional: "group", kind: String.self, optional: true, usage: "Group name to sort. For a nested group specify the full path from parent to child with / inbetween.")) { (sortArguments, group) in + sortArguments.group = group + } + binder.bindXcodeProject(parser: subparser) + binder.bindReferenceSorting(parser: subparser) + binder.bind(option: subparser.add(option: "--recursive", shortName: "-r", kind: Bool.self, usage: "Recursively descend through all child groups.")) { (sortArguments, recursive) in + sortArguments.recursive = recursive + } + } + + public func run(with arguments: ArgumentParser.Result) throws { + var sortArguments = SortArguments() + try binder.fill(parseResult: arguments, into: &sortArguments) + let projectFile = try sortArguments.loadedProject() + let group = try projectFile.group(forPath: sortArguments.group) + group.sort(recursive: sortArguments.recursive, by: sortArguments.order) + try projectFile.save() + } +} diff --git a/Sources/MooseKit/SyncCommand.swift b/Sources/XcoderKit/Commands/SyncCommand.swift similarity index 55% rename from Sources/MooseKit/SyncCommand.swift rename to Sources/XcoderKit/Commands/SyncCommand.swift index 79f51ad..acc7ba6 100644 --- a/Sources/MooseKit/SyncCommand.swift +++ b/Sources/XcoderKit/Commands/SyncCommand.swift @@ -1,29 +1,32 @@ // // SortCommand.swift -// bullwinkle +// xcoder // // Created by Geoffrey Foster on 2018-01-10. // import Foundation import Utility +import Basic import XcodeProject +import CommandRegistry -final class SyncArguments { - var xcodeproj: Foundation.URL = URL(fileURLWithPath: ".") - var group: String? - var targetName: String? - var recursive: Bool = false -} - -class SyncCommand: Command { - let name: String = "sync" - let binder = ArgumentBinder() +public struct SyncCommand: Command { + private struct SyncArguments { + var xcodeproj: AbsolutePath = localFileSystem.currentWorkingDirectory! + var group: String? + var targetName: String? + var recursive: Bool = false + } + + public let command: String = "sync" + public let overview: String = "Sync the Xcode project groups with the filesystem." + private let binder = ArgumentBinder() - required init(parser: ArgumentParser) { - let subparser = parser.add(subparser: name, overview: "Sync the Xcode project groups with the filesystem.") - binder.bind(positional: subparser.add(positional: "xcodeproj", kind: URL.self, optional: false, usage: "Xcode Project file.")) { (syncArguments, xcodeproj) in - syncArguments.xcodeproj = xcodeproj + public init(parser: ArgumentParser) { + let subparser = parser.add(subparser: command, overview: overview) + binder.bind(positional: subparser.add(positional: "xcodeproj", kind: PathArgument.self, optional: false, usage: "Xcode Project file.", completion: .filename)) { (syncArguments, xcodeproj) in + syncArguments.xcodeproj = xcodeproj.path } binder.bind(option: subparser.add(option: "--group", shortName: "-g", kind: String.self, usage: "Group name to sync. For a nested group specify the full path from parent to child with / inbetween.")) { (syncArguments, group) in syncArguments.group = group @@ -36,22 +39,22 @@ class SyncCommand: Command { } } - func execute(parsedArguments: ArgumentParser.Result) throws { + public func run(with parsedArguments: ArgumentParser.Result) throws { var arguments = SyncArguments() - binder.fill(parsedArguments, into: &arguments) + try binder.fill(parseResult: parsedArguments, into: &arguments) let projectFile: ProjectFile do { - projectFile = try ProjectFile(url: arguments.xcodeproj) + projectFile = try ProjectFile(url: Foundation.URL(fileURLWithPath: arguments.xcodeproj.asString)) } catch { - throw Bullwinkle.Error.invalidProject(path: arguments.xcodeproj.path) + throw Error.invalidProject(path: arguments.xcodeproj.asString) } let group = try projectFile.group(forPath: arguments.group) var target: PBXTarget? if let targetName = arguments.targetName { guard let targetNamed = projectFile.project.target(named: targetName) else { - throw Bullwinkle.Error.invalidTarget(targetName) + throw Error.invalidTarget(targetName) } target = targetNamed } diff --git a/Sources/XcoderKit/Error.swift b/Sources/XcoderKit/Error.swift new file mode 100644 index 0000000..2d3f503 --- /dev/null +++ b/Sources/XcoderKit/Error.swift @@ -0,0 +1,29 @@ +// +// Error.swift +// xcoder +// +// Created by Geoffrey Foster on 2018-01-13. +// + +import Foundation + +enum Error: Swift.Error, CustomStringConvertible { + case invalidProject(path: String?) + case invalidGroup(String) + case invalidTarget(String) + + var description: String { + switch self { + case .invalidProject(let path): + if let path = path { + return "Invalid project at \(path)" + } else { + return "No project specified" + } + case .invalidGroup(let groupPath): + return "Invalid group path \(groupPath)" + case .invalidTarget(let targetName): + return "Invalid target \(targetName)" + } + } +} diff --git a/Sources/MooseKit/PBXObject+Extensions.swift b/Sources/XcoderKit/PBXObject+Extensions.swift similarity index 90% rename from Sources/MooseKit/PBXObject+Extensions.swift rename to Sources/XcoderKit/PBXObject+Extensions.swift index 768bb09..56e7e9a 100644 --- a/Sources/MooseKit/PBXObject+Extensions.swift +++ b/Sources/XcoderKit/PBXObject+Extensions.swift @@ -1,6 +1,6 @@ // // PBXObject+Extensions.swift -// bullwinkle +// xcoder // // Created by Geoffrey Foster on 2018-01-11. // @@ -13,7 +13,7 @@ extension ProjectFile { let group: PBXGroup if let path = path { guard let childGroup = project.group(for: path) else { - throw Bullwinkle.Error.invalidGroup(path) + throw Error.invalidGroup(path) } group = childGroup } else { @@ -24,7 +24,7 @@ extension ProjectFile { func buildPhase(named name: String) throws -> [PBXBuildPhase] { guard let target = project.target(named: "") else { - throw Bullwinkle.Error.invalidTarget("") + throw Error.invalidTarget("") } return target.buildPhases.filter { @@ -47,7 +47,7 @@ extension PBXGroup { } var childGroups: [PBXGroup] { - return children.flatMap { $0 as? PBXGroup } + return children.compactMap { $0 as? PBXGroup } } func contains(_ reference: PBXReference, recursive: Bool) -> Bool { diff --git a/Sources/XcoderKit/XcodeProjectLoading.swift b/Sources/XcoderKit/XcodeProjectLoading.swift new file mode 100644 index 0000000..fa853df --- /dev/null +++ b/Sources/XcoderKit/XcodeProjectLoading.swift @@ -0,0 +1,52 @@ +// +// XcodeProjectLoading.swift +// XcoderKit +// +// Created by Geoffrey Foster on 2018-12-10. +// + +import Foundation +import Basic +import Utility +import XcodeProject + +protocol XcodeProjectLoading { + var xcodeproj: AbsolutePath { get set } +} + +extension XcodeProjectLoading { + func loadedProject() throws -> ProjectFile { + func projectURL(from path: AbsolutePath) throws -> Foundation.URL { + if path.extension == "xcodeproj" { + return URL(fileURLWithPath: path.asString) + } + + guard localFileSystem.isDirectory(path) else { + throw Error.invalidProject(path: path.asString) + } + + let dirs = try localFileSystem.getDirectoryContents(path) + if let xcodeproj = dirs.sorted().first(where: { $0.hasSuffix("xcodeproj") }) { + return URL(fileURLWithPath: xcodeproj, relativeTo: URL(fileURLWithPath: path.asString, isDirectory: true)) + } + throw Error.invalidProject(path: path.asString) + } + let xcodeprojURL = try projectURL(from: xcodeproj) + let projectFile: ProjectFile + do { + projectFile = try ProjectFile(url: xcodeprojURL) + } catch { + throw Error.invalidProject(path: xcodeprojURL.path) + } + return projectFile + } +} + +extension ArgumentBinder where Options: XcodeProjectLoading { + func bindXcodeProject(parser: ArgumentParser) { + let option = parser.add(option: "--xcodeproj", shortName: nil, kind: PathArgument.self, usage: "Xcode project file to load", completion: .filename) + bind(option: option) { (options, xcodeproj) in + options.xcodeproj = xcodeproj.path + } + } +} diff --git a/Sources/XcoderKit/XcodeReferenceSorting.swift b/Sources/XcoderKit/XcodeReferenceSorting.swift new file mode 100644 index 0000000..38bbaf7 --- /dev/null +++ b/Sources/XcoderKit/XcodeReferenceSorting.swift @@ -0,0 +1,38 @@ +// +// XcodeReferenceSorting.swift +// XcoderKit +// +// Created by Geoffrey Foster on 2018-12-10. +// + +import Foundation +import Basic +import Utility +import XcodeProject + +extension PBXReference.SortOption: StringEnumArgument { + static var options: [(name: String, description: String)] = [ + (name: PBXReference.SortOption.name.rawValue, description: "Sort by name"), + (name: PBXReference.SortOption.type.rawValue, description: "Sort by type") + ] + public static var completion: ShellCompletion { + return .values(options.map { (value: $0.name, description: $0.description) }) + } +} + +protocol XcodeReferenceSorting { + var order: PBXReference.SortOption { get set } +} + +extension XcodeReferenceSorting { + +} + +extension ArgumentBinder where Options: XcodeReferenceSorting { + func bindReferenceSorting(parser: ArgumentParser) { + let option = parser.add(option: "--order", shortName: "-o", kind: PBXReference.SortOption.self, usage: "Sort the group by one of [\(PBXGroup.SortOption.options.map { $0.name }.joined(separator: ", "))]") + bind(option: option) { (options, order) in + options.order = order + } + } +} diff --git a/Sources/xcoder/Version.swift b/Sources/xcoder/Version.swift new file mode 100644 index 0000000..1e5e950 --- /dev/null +++ b/Sources/xcoder/Version.swift @@ -0,0 +1,5 @@ +import Utility + +extension Version { + static var current: Version = "0.3.0" +} diff --git a/Sources/xcoder/main.swift b/Sources/xcoder/main.swift new file mode 100644 index 0000000..10470d2 --- /dev/null +++ b/Sources/xcoder/main.swift @@ -0,0 +1,11 @@ +import XcoderKit +import Foundation +import CommandRegistry +import Utility + +var registry = Registry(usage: " ", overview: "", version: Version.current) +registry.register(command: CompletionToolCommand.self) +registry.register(command: SyncCommand.self) +registry.register(command: GroupSortCommand.self) +registry.register(command: BuildPhaseSortCommand.self) +registry.run() diff --git a/Version.swift.template b/Version.swift.template new file mode 100644 index 0000000..a6e9e7b --- /dev/null +++ b/Version.swift.template @@ -0,0 +1,5 @@ +import Utility + +extension Version { + static var current: Version = "__VERSION__" +}