From 541b9d68a0e557b63be353c77695456fa6388c5e Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" Date: Fri, 20 Mar 2026 21:37:42 +0100 Subject: [PATCH] Use Shwift library instead of Subprocess Shwift has a concise API, which makes writing shell code nice and easy. This is an opinionated decision. --- Package.resolved | 29 ++++------ Package.swift | 8 +-- Sources/TaskCLI/Command.swift | 2 +- .../GenerateDocumentation Command.swift | 53 +++++++------------ Sources/TaskCLI/Global Options.swift | 2 +- Sources/TaskCLI/Test Command.swift | 33 +++++------- 6 files changed, 46 insertions(+), 81 deletions(-) diff --git a/Package.resolved b/Package.resolved index d045c66..3bf5f8e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fd1e824e418c767633bb79b055a4e84d9c86165746bc881d5d27457ad34b0c20", + "originHash" : "0cb2e87817f52021ac25ffee6b27396f6d94e9fd604ca83db7f20a10e65fe6cf", "pins" : [ { "identity" : "noora", @@ -28,6 +28,15 @@ "version" : "4.2.1" } }, + { + "identity" : "shwift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GeorgeLyon/Shwift", + "state" : { + "revision" : "d7be04898d094ddce6140cc6a2e9a83fc994b66d", + "version" : "3.1.1" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", @@ -37,15 +46,6 @@ "version" : "1.7.0" } }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", - "state" : { - "revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272", - "version" : "1.1.3" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -82,15 +82,6 @@ "version" : "2.95.0" } }, - { - "identity" : "swift-subprocess", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-subprocess.git", - "state" : { - "revision" : "ba5888ad7758cbcbe7abebac37860b1652af2d9c", - "version" : "0.3.0" - } - }, { "identity" : "swift-system", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 134488d..5729d1e 100644 --- a/Package.swift +++ b/Package.swift @@ -11,12 +11,10 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.0"), - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.1.3"), .package(url: "https://github.com/apple/swift-log", from: "1.10.1"), .package(url: "https://github.com/apple/swift-nio", from: "2.95.0"), .package(url: "https://github.com/apple/swift-system", from: "1.6.4"), - .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.3.0"), + .package(url: "https://github.com/GeorgeLyon/Shwift", from: "3.1.1"), .package(url: "https://github.com/tuist/Noora", from: "0.55.1") ], targets: [ @@ -40,11 +38,9 @@ let package = Package( .executableTarget( name: "InotifyTaskCLI", dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "Logging", package: "swift-log"), .product(name: "_NIOFileSystem", package: "swift-nio"), - .product(name: "Subprocess", package: "swift-subprocess"), + .product(name: "Script", package: "Shwift"), .product(name: "Noora", package: "Noora") ], path: "Sources/TaskCLI" diff --git a/Sources/TaskCLI/Command.swift b/Sources/TaskCLI/Command.swift index 20d9324..8f04cd4 100644 --- a/Sources/TaskCLI/Command.swift +++ b/Sources/TaskCLI/Command.swift @@ -1,4 +1,4 @@ -import ArgumentParser +import Script @main struct Command: AsyncParsableCommand { diff --git a/Sources/TaskCLI/GenerateDocumentation Command.swift b/Sources/TaskCLI/GenerateDocumentation Command.swift index 8f5c2aa..91a36c5 100644 --- a/Sources/TaskCLI/GenerateDocumentation Command.swift +++ b/Sources/TaskCLI/GenerateDocumentation Command.swift @@ -1,11 +1,9 @@ -import ArgumentParser -import AsyncAlgorithms import Foundation import Logging +import Script import Noora -import Subprocess -struct GenerateDocumentationCommand: AsyncParsableCommand { +struct GenerateDocumentationCommand: Script { static let configuration = CommandConfiguration( commandName: "generate-documentation", abstract: "Generate DocC documentation of all targets inside a Linux container.", @@ -25,6 +23,7 @@ struct GenerateDocumentationCommand: AsyncParsableCommand { let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation") let fileManager = FileManager.default let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath) + let docker = try await executable(named: "docker") let targets = try await Self.targets(for: projectDirectory) @@ -43,28 +42,16 @@ struct GenerateDocumentationCommand: AsyncParsableCommand { let script = Self.makeRunScript(for: targets) logger.debug("Container script", metadata: ["script": "\(script)"]) - let dockerResult = try await Subprocess.run( - .name("docker"), - arguments: [ + do { + try await docker( "run", "--rm", - "-v", "\(tempDirectory.path(percentEncoded: false)):/code", + "-v", "\(tempDirectory.path):/code", "--platform", "linux/arm64", "-w", "/code", "swift:latest", "/bin/bash", "-c", script, - ], - preferredBufferSize: 10, - ) { execution, standardInput, standardOutput, standardError in - print("") - let stdout = standardOutput.lines() - let stderr = standardError.lines() - for try await line in merge(stdout, stderr) { - noora.passthrough("\(line)") - } - print("") - } - - guard dockerResult.terminationStatus.isSuccess else { + ) + } catch { noora.error("Documentation generation failed.") return } @@ -116,11 +103,10 @@ struct GenerateDocumentationCommand: AsyncParsableCommand { } private static func packageTargets() async throws -> [(name: String, path: String)] { - let packageDescription = try await Subprocess.run( - .name("swift"), - arguments: ["package", "describe", "--type", "json"], - output: .data(limit: 20_000) - ) + let swift = try await executable(named: "swift") + let packageDescriptionOutput = try await outputOf { + try await swift("package", "describe", "--type", "json") + } struct PackageDescription: Codable { let targets: [Target] @@ -130,7 +116,8 @@ struct GenerateDocumentationCommand: AsyncParsableCommand { let path: String } - let package = try JSONDecoder().decode(PackageDescription.self, from: packageDescription.standardOutput) + let data = Data(packageDescriptionOutput.utf8) + let package = try JSONDecoder().decode(PackageDescription.self, from: data) return package.targets.map { ($0.name, $0.path) } } @@ -175,15 +162,13 @@ struct GenerateDocumentationCommand: AsyncParsableCommand { // MARK: - Dependency Injection private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws { - let result = try await Subprocess.run( - .name("swift"), - arguments: [ + let swift = try await executable(named: "swift") + do { + try await swift( "package", "--package-path", directory.path(percentEncoded: false), "add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL - ], - ) { _ in } - - guard result.terminationStatus.isSuccess else { + ) + } catch { throw GenerateDocumentationError.dependencyInjectionFailed } diff --git a/Sources/TaskCLI/Global Options.swift b/Sources/TaskCLI/Global Options.swift index f96ab36..8dd20f7 100644 --- a/Sources/TaskCLI/Global Options.swift +++ b/Sources/TaskCLI/Global Options.swift @@ -1,4 +1,4 @@ -import ArgumentParser +import Script import Logging struct GlobalOptions: ParsableArguments { diff --git a/Sources/TaskCLI/Test Command.swift b/Sources/TaskCLI/Test Command.swift index d06a23d..7e02d39 100644 --- a/Sources/TaskCLI/Test Command.swift +++ b/Sources/TaskCLI/Test Command.swift @@ -1,10 +1,8 @@ -import ArgumentParser -import AsyncAlgorithms import Foundation -import Subprocess +import Script import Noora -struct TestCommand: AsyncParsableCommand { +struct TestCommand: Script { static let configuration = CommandConfiguration( commandName: "test", abstract: "Run swift test in a linux container.", @@ -19,26 +17,21 @@ struct TestCommand: AsyncParsableCommand { let noora = Noora() let logger = global.makeLogger(labeled: "swift-inotify.cli.task.test") let currentDirectory = FileManager.default.currentDirectoryPath + let docker = Executable(path: "/opt/homebrew/bin/docker") noora.info("Running tests on Linux.") logger.debug("Current directory", metadata: ["current-directory": "\(currentDirectory)"]) - async let monitorResult = Subprocess.run( - .name("docker"), - arguments: ["run", "-v", "\(currentDirectory):/code", "--security-opt", "systempaths=unconfined", "--platform", "linux/arm64", "-w", "/code", "swift:latest", "/bin/bash", "-c", "swift test --skip InotifyLimitTests; swift test --skip-build --filter InotifyLimitTests"], - preferredBufferSize: 10, - ) { execution, standardInput, standardOutput, standardError in - print("") - let stdout = standardOutput.lines() - let stderr = standardError.lines() - for try await line in merge(stdout, stderr) { - noora.passthrough("\(line)") - } - print("") - } - - if (try await monitorResult.terminationStatus.isSuccess) { + do { + try await docker( + "run", + "-v", "\(currentDirectory):/code", + "--security-opt", "systempaths=unconfined", + "--platform", "linux/arm64", + "-w", "/code", "swift:latest", + "/bin/bash", "-c", "swift test --skip InotifyLimitTests; swift test --skip-build --filter InotifyLimitTests" + ) noora.success("All tests completed successfully.") - } else { + } catch { noora.error("Not all tests completed successfully.") } }