As iOS development, our CI often uses Ruby command-line tools like Fastlane, CocoaPods, and Xcodeproj.

As Ruby faded, maintenance costs rose.

Through Swift Package Manager, use Apple Swift language to establish Command line tool, making iOS developers in the team easier to develop and maintain.

An example: Creating a xcode helper

Create an example using Swift Package Manager to view xcode cache files. As shown in figure:

Creating a command-line tool


mkdir xcode-helper && cd xcode-helper

swift package init --type executable

Copy the code

type

  • Library Creates library.

  • Executable. Create command line tools.

Build and run an executable product

Command line operation


swift run

Copy the code

> swift run

[3/3] Linking xcode-helper

* Build Completed!

Hello, world!

Copy the code

Run it using Xcode


swift package generate-xcodeproj

open *.xcodeproj

Copy the code

Adding dependencies

Add apple/swift-argument-parser to get command line arguments.


vi Package.swift

Copy the code
Package (url: "https://github.com/apple/swift-argument-parser", from: "0.4.0")Copy the code

Include “ArgumentParser” as a dependency for your executable target:


.product(name: "ArgumentParser", package: "swift-argument-parser"),

Copy the code

Package.swift Example:

Swift-tools-version :5.3 // The swift-tools-version declares The minimum version of swift required to build this package. import PackageDescription let package = Package( name: "xcode-helper", dependencies: [ .package( url: "Https://github.com/apple/swift-argument-parser" from: "0.4.0")], the targets: [. Target (name: "xcode-helper", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), .testTarget( name: "xcode-helperTests", dependencies: ["xcode-helper"]), ] )Copy the code

Installing dependencies

After modification, pull dependencies through Swift Package update


swift package update

Copy the code

Creating the main execution command

Sources/

/main.swift, add processing logic


vi Sources/xcode-helper/main.swift

Copy the code
Import Foundation import ArgumentParser struct Constant {struct App {static let version = "0.0.1"}} @discardableResult func shell(_ command: String) -> String { let task = Process() let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.arguments = ["-c", command] task.launchPath = "/bin/zsh" task.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)! return output } struct Print { enum Color: String { case reset = "\u{001B}[0;0m" case black = "\u{001B}[0;30m" case red = "\u{001B}[0;31m" case green = "\u{001B}[0;32m" case yellow = "\u{001B}[0;33m" case blue = "\u{001B}[0;34m" case magenta = "\u{001B}[0;35m" case cyan =  "\u{001B}[0;36m" case white = "\u{001B}[0;37m" } static func h3(_ items: Any..., separator: String = " ", terminator: String = "\n") { // https://stackoverflow.com/questions/39026752/swift-extending-functionality-of-print-function let output = items.map { "\($0)" }.joined(separator: separator) print("\(Color.green.rawValue)\(output)\(Color.reset.rawValue)") } static func h6(_ verbose: Bool, _ items: Any..., separator: String = " ", terminator: String = "\n") { if verbose { let output = items.map { "\($0)" }.joined(separator: separator) print("\(output)") } } } extension XcodeHelper { enum CacheFolder: String, ExpressibleByArgument, CaseIterable { case all case archives case simulators case deviceSupport case derivedData case previews case coreSimulatorCaches } } fileprivate extension XcodeHelper.CacheFolder { var paths: [String] { switch self { case .archives: return ["~/Library/Developer/Xcode/Archives"] case .simulators: return ["~/Library/Developer/CoreSimulator/Devices"] case .deviceSupport: return ["~/Library/Developer/Xcode"] case .derivedData: return ["~/Library/Developer/Xcode/DerivedData"] case .previews: return ["~/Library/Developer/Xcode/UserData/Previews/Simulator Devices"] case .coreSimulatorCaches: return ["~/Library/Developer/CoreSimulator/Caches/dyld"] case .all: var paths: [String] = [] for caseValue in Self.allCases { if caseValue != self { paths.append(contentsOf: caseValue.paths) } } return paths } } static var suggestion: String { let suggestion = Self.allCases.map { caseValue in return caseValue.rawValue }.joined(separator: " | ") return "[ \(suggestion) ]" } } struct XcodeHelper: ParsableCommand { public static let configuration = CommandConfiguration( abstract: "Xcode helper", version: "xcode-helper version \(Constant.App.version)", subcommands: [ Cache.self ] ) } extension XcodeHelper { struct Cache: ParsableCommand { public static let configuration = CommandConfiguration( abstract: "Xcode cache helper", subcommands: [ List.self ] ) } } extension XcodeHelper.Cache { struct List: ParsableCommand { public static let configuration = CommandConfiguration( abstract: "Show Xcode cache files" ) @Option(name: .shortAndLong, help: "The cache folder") private var cacheFolder: XcodeHelper.CacheFolder = .all @Flag(name: .shortAndLong, help: "Show extra logging for debugging purposes.") private var verbose: Bool = false func run() throws { Print.h3("list cache files:") Print.h3("------------------------") if cacheFolder == .all { var allCases = XcodeHelper.CacheFolder.allCases allCases.remove(at: allCases.firstIndex(of: .all)!) handleList(allCases) } else { handleList([cacheFolder]) } } private func handleList(_ folders: [XcodeHelper.CacheFolder]) { for folder in folders { Print.h3(folder.rawValue) for path in folder.paths { let cmd = "du -hs \(path)" Print.h6(verbose, cmd) let output = shell(cmd) print(output) } } } } } XcodeHelper.main()Copy the code

Build and run an executable product

Get all targets

Gets all targets under the current project.

python3 -c "\ import sys, json, subprocess; \ package_data = subprocess.Popen('swift package dump-package', shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8'); \ targets = json.loads(package_data)['targets']; \ target_names = list(map(lambda x: x['name'], targets)); \ print(target_names)\ "Copy the code

Start using command-line

Use swift Run

to see the effect


swift run xcode-helper

Copy the code

Start using subcommand

To ensure the expansion of Xcode-Helper, cache is implemented as a subcommand


swift run xcode-helper cache list

Copy the code

Writing Unit testing

Tests/

Tests/

tests. swift to add the necessary unit Tests.


vi Tests/xcode-helperTests/xcode_helperTests.swift

Copy the code
import XCTest import class Foundation.Bundle extension XCTest { public var debugURL: URL { let bundleURL = Bundle(for: type(of: self)).bundleURL return bundleURL.lastPathComponent.hasSuffix("xctest") ? bundleURL.deletingLastPathComponent() : bundleURL } public func AssertExecuteCommand( command: String, expected: String? = nil, exitCode: Int32 = EXIT_SUCCESS, file: StaticString = #file, line: UInt = #line) { let splitCommand = command.split(separator: " ") let arguments = splitCommand.dropFirst().map(String.init) let commandName = String(splitCommand.first!) let commandURL = debugURL.appendingPathComponent(commandName) guard (try? commandURL.checkResourceIsReachable()) ?? false else { XCTFail("No executable at '\(commandURL.standardizedFileURL.path)'.", file: (file), line: Line) return} let process = process () if #available(macOS 10.13, *) { process.executableURL = commandURL } else { process.launchPath = commandURL.path } process.arguments = arguments let output = Pipe() process.standardOutput = output let error = Pipe() process.standardError = error if #available(macOS 10.13, *) {guard (try? process.run()) ! = nil else { XCTFail("Couldn't run command process.", file: (file), line: line) return } } else { process.launch() } process.waitUntilExit() let outputData = output.fileHandleForReading.readDataToEndOfFile() let outputActual = String(data: outputData, encoding: .utf8)! .trimmingCharacters(in: .whitespacesAndNewlines) let errorData = error.fileHandleForReading.readDataToEndOfFile() let errorActual = String(data:  errorData, encoding: .utf8)! .trimmingCharacters(in: .whitespacesAndNewlines) if let expected = expected { XCTAssertEqual(expected, errorActual + outputActual) } XCTAssertEqual(process.terminationStatus, exitCode, file: (file), line: line) } } final class xcode_helperTests: XCTestCase { func test_Xcode_Helper_Versions() throws { AssertExecuteCommand(command: "xcode-helper --version", expected: "Xcode-helper version 0.0.1")} func test_Xcode_Helper_Help() throws {let helpText = """ OVERVIEW: Xcode Helper USAGE: xcode-helper <subcommand> OPTIONS: --version Show the version. -h, --help Show help information. SUBCOMMANDS: cache Xcode cache helper See 'xcode-helper help <subcommand>' for detailed help. """ AssertExecuteCommand(command: "xcode-helper", expected: helpText) AssertExecuteCommand(command: "xcode-helper -h", expected: helpText) AssertExecuteCommand(command: "xcode-helper --help", expected: helpText) } }Copy the code

Run unit tests through Swift Test.


swift test

Copy the code
> swift test test Suite 'All tests started at the 2021-07-17 14:01:47. 357 test Suite' xcode - helperPackageTests. Xctest ' Started at 2021-07-17 14:01:47.358 Test Suite 'xcode_helperTests' started at 2021-07-17 14:01:47.358 Test Case '-[xcode_helperTests.xcode_helperTests test_Xcode_Helper_Help]' started. Test Case '-[xcode_helpertests. xcode_helperTests test_Xcode_Helper_Help]' passed (0.202 seconds).test Case '-[xcode_helperTests.xcode_helperTests test_Xcode_Helper_Versions]' started. Test Case '-[xcode_helpertests. xcode_helperTests test_Xcode_Helper_Versions]' passed (0.074 seconds).test Suite 'xcode_helperTests' passed at 2021-07-17 14:01:47.634. Executed 2 tests, Unexpected failures with 0 (0) in 0.276 seconds (0.276) Test Suite 'xcode - helperPackageTests. Xctest' passed the at Executed 2 tests, With 0 failures (0 unexpected) in 0.276 (0.276) seconds Test Suite 'All tests' passed at 2021-07-17 14:01:47.634. Executed 2 tests, with 0 failures (0 unexpected) in 0.276 (0.277) secondsCopy the code

You can also run tests using Xcode Command -u.

Installing your command line tool

The test passes, release is packaged and moved to /usr/local/bin.


swift build -c release

cp -f .build/release/xcode-helper /usr/local/bin/xcode-helper

xcode-helper --version

Copy the code

> xcode-helper --version

xcode-helper version 0.0.1

Copy the code

Demo

  • Github.com/QiuZhiFei/x…

References

  • Github.com/apple/swift…

  • www.avanderlee.com/swift/comma…

  • www.swiftbysundell.com/articles/bu…

  • Docs.swift.org/package-man…

  • Developer.apple.com/swift/blog/…

  • swift-argument-parser Documentation

  • Github.com/apple/swift…