Use Shwift library instead of Subprocess
Some checks failed
Docs / docs (push) Has been cancelled
Docs / deploy (push) Has been cancelled

Shwift has a concise API, which makes writing shell code nice and easy.
This is an opinionated decision.
This commit is contained in:
T. R. Bernstein
2026-03-20 21:37:42 +01:00
parent 55f3ca2f7b
commit 541b9d68a0
6 changed files with 46 additions and 81 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "fd1e824e418c767633bb79b055a4e84d9c86165746bc881d5d27457ad34b0c20", "originHash" : "0cb2e87817f52021ac25ffee6b27396f6d94e9fd604ca83db7f20a10e65fe6cf",
"pins" : [ "pins" : [
{ {
"identity" : "noora", "identity" : "noora",
@@ -28,6 +28,15 @@
"version" : "4.2.1" "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", "identity" : "swift-argument-parser",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -37,15 +46,6 @@
"version" : "1.7.0" "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", "identity" : "swift-atomics",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -82,15 +82,6 @@
"version" : "2.95.0" "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", "identity" : "swift-system",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -11,12 +11,10 @@ let package = Package(
) )
], ],
dependencies: [ 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-log", from: "1.10.1"),
.package(url: "https://github.com/apple/swift-nio", from: "2.95.0"), .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/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") .package(url: "https://github.com/tuist/Noora", from: "0.55.1")
], ],
targets: [ targets: [
@@ -40,11 +38,9 @@ let package = Package(
.executableTarget( .executableTarget(
name: "InotifyTaskCLI", name: "InotifyTaskCLI",
dependencies: [ dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "Logging", package: "swift-log"), .product(name: "Logging", package: "swift-log"),
.product(name: "_NIOFileSystem", package: "swift-nio"), .product(name: "_NIOFileSystem", package: "swift-nio"),
.product(name: "Subprocess", package: "swift-subprocess"), .product(name: "Script", package: "Shwift"),
.product(name: "Noora", package: "Noora") .product(name: "Noora", package: "Noora")
], ],
path: "Sources/TaskCLI" path: "Sources/TaskCLI"

View File

@@ -1,4 +1,4 @@
import ArgumentParser import Script
@main @main
struct Command: AsyncParsableCommand { struct Command: AsyncParsableCommand {

View File

@@ -1,11 +1,9 @@
import ArgumentParser
import AsyncAlgorithms
import Foundation import Foundation
import Logging import Logging
import Script
import Noora import Noora
import Subprocess
struct GenerateDocumentationCommand: AsyncParsableCommand { struct GenerateDocumentationCommand: Script {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "generate-documentation", commandName: "generate-documentation",
abstract: "Generate DocC documentation of all targets inside a Linux container.", 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 logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation")
let fileManager = FileManager.default let fileManager = FileManager.default
let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath) let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath)
let docker = try await executable(named: "docker")
let targets = try await Self.targets(for: projectDirectory) let targets = try await Self.targets(for: projectDirectory)
@@ -43,28 +42,16 @@ struct GenerateDocumentationCommand: AsyncParsableCommand {
let script = Self.makeRunScript(for: targets) let script = Self.makeRunScript(for: targets)
logger.debug("Container script", metadata: ["script": "\(script)"]) logger.debug("Container script", metadata: ["script": "\(script)"])
let dockerResult = try await Subprocess.run( do {
.name("docker"), try await docker(
arguments: [
"run", "--rm", "run", "--rm",
"-v", "\(tempDirectory.path(percentEncoded: false)):/code", "-v", "\(tempDirectory.path):/code",
"--platform", "linux/arm64", "--platform", "linux/arm64",
"-w", "/code", "-w", "/code",
"swift:latest", "swift:latest",
"/bin/bash", "-c", script, "/bin/bash", "-c", script,
], )
preferredBufferSize: 10, } catch {
) { 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 {
noora.error("Documentation generation failed.") noora.error("Documentation generation failed.")
return return
} }
@@ -116,11 +103,10 @@ struct GenerateDocumentationCommand: AsyncParsableCommand {
} }
private static func packageTargets() async throws -> [(name: String, path: String)] { private static func packageTargets() async throws -> [(name: String, path: String)] {
let packageDescription = try await Subprocess.run( let swift = try await executable(named: "swift")
.name("swift"), let packageDescriptionOutput = try await outputOf {
arguments: ["package", "describe", "--type", "json"], try await swift("package", "describe", "--type", "json")
output: .data(limit: 20_000) }
)
struct PackageDescription: Codable { struct PackageDescription: Codable {
let targets: [Target] let targets: [Target]
@@ -130,7 +116,8 @@ struct GenerateDocumentationCommand: AsyncParsableCommand {
let path: String 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) } return package.targets.map { ($0.name, $0.path) }
} }
@@ -175,15 +162,13 @@ struct GenerateDocumentationCommand: AsyncParsableCommand {
// MARK: - Dependency Injection // MARK: - Dependency Injection
private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws { private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws {
let result = try await Subprocess.run( let swift = try await executable(named: "swift")
.name("swift"), do {
arguments: [ try await swift(
"package", "--package-path", directory.path(percentEncoded: false), "package", "--package-path", directory.path(percentEncoded: false),
"add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL "add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
], )
) { _ in } } catch {
guard result.terminationStatus.isSuccess else {
throw GenerateDocumentationError.dependencyInjectionFailed throw GenerateDocumentationError.dependencyInjectionFailed
} }

View File

@@ -1,4 +1,4 @@
import ArgumentParser import Script
import Logging import Logging
struct GlobalOptions: ParsableArguments { struct GlobalOptions: ParsableArguments {

View File

@@ -1,10 +1,8 @@
import ArgumentParser
import AsyncAlgorithms
import Foundation import Foundation
import Subprocess import Script
import Noora import Noora
struct TestCommand: AsyncParsableCommand { struct TestCommand: Script {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "test", commandName: "test",
abstract: "Run swift test in a linux container.", abstract: "Run swift test in a linux container.",
@@ -19,26 +17,21 @@ struct TestCommand: AsyncParsableCommand {
let noora = Noora() let noora = Noora()
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.test") let logger = global.makeLogger(labeled: "swift-inotify.cli.task.test")
let currentDirectory = FileManager.default.currentDirectoryPath let currentDirectory = FileManager.default.currentDirectoryPath
let docker = Executable(path: "/opt/homebrew/bin/docker")
noora.info("Running tests on Linux.") noora.info("Running tests on Linux.")
logger.debug("Current directory", metadata: ["current-directory": "\(currentDirectory)"]) logger.debug("Current directory", metadata: ["current-directory": "\(currentDirectory)"])
async let monitorResult = Subprocess.run( do {
.name("docker"), try await 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"], "run",
preferredBufferSize: 10, "-v", "\(currentDirectory):/code",
) { execution, standardInput, standardOutput, standardError in "--security-opt", "systempaths=unconfined",
print("") "--platform", "linux/arm64",
let stdout = standardOutput.lines() "-w", "/code", "swift:latest",
let stderr = standardError.lines() "/bin/bash", "-c", "swift test --skip InotifyLimitTests; swift test --skip-build --filter InotifyLimitTests"
for try await line in merge(stdout, stderr) { )
noora.passthrough("\(line)")
}
print("")
}
if (try await monitorResult.terminationStatus.isSuccess) {
noora.success("All tests completed successfully.") noora.success("All tests completed successfully.")
} else { } catch {
noora.error("Not all tests completed successfully.") noora.error("Not all tests completed successfully.")
} }
} }