From 6874c53fdcc97b288aa8d6dded80051abd8716bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20K=C3=A5gedal=20Reimer?= Date: Mon, 11 Feb 2019 09:15:18 +0100 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ LICENSE | 21 ++++++ Package.resolved | 52 +++++++++++++++ Package.swift | 19 ++++++ README.md | 25 +++++++ Sources/xcodeproj-modify/Arguments.swift | 65 +++++++++++++++++++ Sources/xcodeproj-modify/Command.swift | 5 ++ .../xcodeproj-modify/XcodeprojModify.swift | 61 +++++++++++++++++ Sources/xcodeproj-modify/main.swift | 4 ++ Tests/LinuxMain.swift | 7 ++ .../XCTestManifests.swift | 9 +++ .../xcodeproj_modifyTests.swift | 47 ++++++++++++++ 12 files changed, 319 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/xcodeproj-modify/Arguments.swift create mode 100644 Sources/xcodeproj-modify/Command.swift create mode 100644 Sources/xcodeproj-modify/XcodeprojModify.swift create mode 100644 Sources/xcodeproj-modify/main.swift create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/xcodeproj-modifyTests/XCTestManifests.swift create mode 100644 Tests/xcodeproj-modifyTests/xcodeproj_modifyTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02c0875 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5941b78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2019 Simon Kågedal Reimer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..d9ce4af --- /dev/null +++ b/Package.resolved @@ -0,0 +1,52 @@ +{ + "object": { + "pins": [ + { + "package": "AEXML", + "repositoryURL": "https://github.com/tadija/AEXML", + "state": { + "branch": null, + "revision": "54bb8ea6fb693dd3f92a89e5fcc19e199fdeedd0", + "version": "4.3.3" + } + }, + { + "package": "PathKit", + "repositoryURL": "https://github.com/kylef/PathKit", + "state": { + "branch": null, + "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", + "version": "0.9.2" + } + }, + { + "package": "Spectre", + "repositoryURL": "https://github.com/kylef/Spectre.git", + "state": { + "branch": null, + "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", + "version": "0.9.0" + } + }, + { + "package": "SwiftShell", + "repositoryURL": "https://github.com/kareman/SwiftShell", + "state": { + "branch": null, + "revision": "beebe43c986d89ea5359ac3adcb42dac94e5e08a", + "version": "4.1.2" + } + }, + { + "package": "xcodeproj", + "repositoryURL": "https://github.com/tuist/xcodeproj.git", + "state": { + "branch": null, + "revision": "4d31d2be2532e9213d58cd4e0b15588c5dfae42d", + "version": "6.5.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..037fa2d --- /dev/null +++ b/Package.swift @@ -0,0 +1,19 @@ +// swift-tools-version:4.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "xcodeproj-modify", + dependencies: [ + .package(url: "https://github.com/tuist/xcodeproj.git", from: "6.2.0"), + ], + targets: [ + .target( + name: "xcodeproj-modify", + dependencies: ["xcodeproj"]), + .testTarget( + name: "xcodeproj-modifyTests", + dependencies: ["xcodeproj-modify"]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..31852ad --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# xcodeproj-modify + +This microtool, despite its very generic name, currently only performs one very specific task: adds a Run Script phase to an Xcode project. + +The use case is when you have an ephemeral Xcode project that is regenerated by Swift Package Manager and you want to add a Run Script phase to integrate SwiftLint. + +## Usage + +```shell +$ xcodeproj-modify MyProject.xcodeproj add-run-script-phase MyTarget swiftlint +``` + +This will edit your Xcode project `MyProject.xcodeproj` and add a Run Script phase to the `MyTarget` target that runs swiftlint. Since you specify the script as a command line parameter to the tool, it gets unwieldy if it's a lot of code, so in that case I'd recommend putting it in a script and pass _that_ as the code to add-run-script-phase. + + +## Installation + +Using Mint: + +```shell +$ mint install skagedal/xcodeproj-modify +``` + +You may also add the tool as a SPM dependency in your Package.swift and then run it with `swift run xcodeproj-modify`. + diff --git a/Sources/xcodeproj-modify/Arguments.swift b/Sources/xcodeproj-modify/Arguments.swift new file mode 100644 index 0000000..e0c8fbf --- /dev/null +++ b/Sources/xcodeproj-modify/Arguments.swift @@ -0,0 +1,65 @@ +import Foundation + +struct Arguments { + let xcodeprojPath: String + let commands: [Command] + + enum Error: LocalizedError { + case missingXcodeprojPath + case missingCommand + case unknownCommand(String) + case missingTarget + case missingContents + + var errorDescription: String? { + switch self { + case .missingXcodeprojPath: + return "Please give path to xcodeproj as first argument" + case .missingCommand: + return "Please give a command (add-run-script-phase)" + case .unknownCommand(let command): + return "Unknown command: \(command)" + case .missingTarget: + return "Please specify a target as the first argument after add-run-script-phase" + case .missingContents: + return "Please specify shell script contents as the second argument after add-run-script-phase" + } + } + } + + private typealias TokenIterator = IndexingIterator> + + init(_ arguments: [String]) throws { + var iterator = arguments.dropFirst().makeIterator() + + guard let path = iterator.next() else { + throw Error.missingXcodeprojPath + } + xcodeprojPath = path + + guard let commandName = iterator.next() else { + throw Error.missingCommand + } + let command = try Arguments.parseCommand(named: commandName, from: &iterator) + commands = [command] + } + + private static func parseCommand(named commandName: String, from iterator: inout TokenIterator) throws -> Command { + switch commandName { + case "add-run-script-phase": + guard let target = iterator.next() else { + throw Error.missingTarget + } + guard let contents = iterator.next() else { + throw Error.missingContents + } + return Command.addRunScriptPhase(target: target, contents: contents) + + default: + throw Error.unknownCommand(commandName) + } + } + +} + + diff --git a/Sources/xcodeproj-modify/Command.swift b/Sources/xcodeproj-modify/Command.swift new file mode 100644 index 0000000..62544b3 --- /dev/null +++ b/Sources/xcodeproj-modify/Command.swift @@ -0,0 +1,5 @@ +import Foundation + +enum Command { + case addRunScriptPhase(target: String, contents: String) +} diff --git a/Sources/xcodeproj-modify/XcodeprojModify.swift b/Sources/xcodeproj-modify/XcodeprojModify.swift new file mode 100644 index 0000000..43d6fa8 --- /dev/null +++ b/Sources/xcodeproj-modify/XcodeprojModify.swift @@ -0,0 +1,61 @@ +import Foundation +import PathKit +import xcodeproj + +struct XcodeprojModify { + enum Error: LocalizedError { + case unknownTarget(String) + + var errorDescription: String? { + switch self { + case .unknownTarget(let string): + return "Couldn't find target named \(string)" + } + } + } + + private let arguments: [String] + + init(arguments: [String]) { + self.arguments = arguments + } + + public func run() -> Int32 { + do { + let arguments = try Arguments(self.arguments) + try run(with: arguments) + } catch { + print(error.localizedDescription) + return 1 + } + return 0 + } + + private func run(with arguments: Arguments) throws { + for command in arguments.commands { + try runCommand(command, with: arguments) + } + } + + private func runCommand(_ command: Command, with arguments: Arguments) throws { + switch command { + case .addRunScriptPhase(let target, let contents): + try addRunScriptPhase(target: target, contents: contents, xcodeprojPath: arguments.xcodeprojPath) + } + } + + private func addRunScriptPhase(target: String, contents: String, xcodeprojPath: String) throws { + let path = Path(xcodeprojPath) + let xcodeproj = try XcodeProj(path: path) + let targets = xcodeproj.pbxproj.targets(named: target) + guard !targets.isEmpty else { + throw Error.unknownTarget(target) + } + + let phase = PBXShellScriptBuildPhase(shellScript: contents) + for target in targets { + target.buildPhases = target.buildPhases + [phase] + } + try xcodeproj.write(path: path) + } +} diff --git a/Sources/xcodeproj-modify/main.swift b/Sources/xcodeproj-modify/main.swift new file mode 100644 index 0000000..3250d37 --- /dev/null +++ b/Sources/xcodeproj-modify/main.swift @@ -0,0 +1,4 @@ +import Foundation + +exit(XcodeprojModify(arguments: CommandLine.arguments).run()) + diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..06db335 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,7 @@ +import XCTest + +import xcodeproj_modifyTests + +var tests = [XCTestCaseEntry]() +tests += xcodeproj_modifyTests.allTests() +XCTMain(tests) \ No newline at end of file diff --git a/Tests/xcodeproj-modifyTests/XCTestManifests.swift b/Tests/xcodeproj-modifyTests/XCTestManifests.swift new file mode 100644 index 0000000..926e43a --- /dev/null +++ b/Tests/xcodeproj-modifyTests/XCTestManifests.swift @@ -0,0 +1,9 @@ +import XCTest + +#if !os(macOS) +public func allTests() -> [XCTestCaseEntry] { + return [ + testCase(xcodeproj_modifyTests.allTests), + ] +} +#endif \ No newline at end of file diff --git a/Tests/xcodeproj-modifyTests/xcodeproj_modifyTests.swift b/Tests/xcodeproj-modifyTests/xcodeproj_modifyTests.swift new file mode 100644 index 0000000..d5ee017 --- /dev/null +++ b/Tests/xcodeproj-modifyTests/xcodeproj_modifyTests.swift @@ -0,0 +1,47 @@ +import XCTest +import class Foundation.Bundle + +final class xcodeproj_modifyTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + + // Some of the APIs that we use below are available in macOS 10.13 and above. + guard #available(macOS 10.13, *) else { + return + } + + let fooBinary = productsDirectory.appendingPathComponent("xcodeproj_modify") + + let process = Process() + process.executableURL = fooBinary + + let pipe = Pipe() + process.standardOutput = pipe + + try process.run() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + + XCTAssertEqual(output, "Hello, world!\n") + } + + /// Returns path to the built products directory. + var productsDirectory: URL { + #if os(macOS) + for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { + return bundle.bundleURL.deletingLastPathComponent() + } + fatalError("couldn't find the products directory") + #else + return Bundle.main.bundleURL + #endif + } + + static var allTests = [ + ("testExample", testExample), + ] +}