Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
// swift-tools-version:5.9
// swift-tools-version:6.1

import PackageDescription

let package = Package(
name: "SwiftTools",
platforms: [.macOS(.v11)],
platforms: [.macOS(.v14)],
products: [
.library(name: "SwiftTools", targets: ["SwiftTools"]),
],
dependencies: [
.package(url: "https://github.com/Swinject/Swinject.git", from: "2.8.1"),
.package(url: "https://github.com/Swinject/SwinjectAutoregistration", from: "2.8.1"),
.package(url: "https://github.com/jakeheis/SwiftCLI", exact: "6.0.3"),
.package(url: "https://github.com/cpisciotta/xcbeautify", exact: "2.8.0"),
.package(url: "https://github.com/raptorxcz/xcbeautify", revision: "f2204485ba19bb36c4e62f4245a7b6f41d7d687a"),
],
targets: [
.target(name: "SwiftTools", dependencies: [
"Swinject",
"SwinjectAutoregistration",
"SwiftCLI",
.product(name: "XcbeautifyLib", package: "xcbeautify"),
]),
.testTarget(
Expand Down
33 changes: 29 additions & 4 deletions Sources/SwiftTools/Build/Domain/BuildInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import XcbeautifyLib

public protocol BuildInteractor {
func build(with arguments: BuildArguments) throws
Expand Down Expand Up @@ -56,7 +57,34 @@ final class BuildInteractorImpl: BuildInteractor {

func test(with arguments: TestArguments) throws {
let arguments = try makeArguments(from: arguments)
try shellService.executeWithXCBeautify(arguments: arguments)

let parser = XCBeautifier(
colored: true,
renderer: .terminal,
preserveUnbeautifiedLines: true,
additionalLines: { nil }
)

let output = OutputHandler(quiet: false, quieter: true, isCI: false) { line in
let normalizedLine = line.trimmingCharacters(in: .whitespacesAndNewlines)
if !normalizedLine.isEmpty {
if !normalizedLine.hasPrefix("Executed") && !normalizedLine.hasPrefix("Test Suite") {
print(line)
}
}
}

try shellService.executeWithProcessing(
arguments: arguments,
onProcessLine: { line in
guard let result = parser.process(line: line) else {
output.write(.undefined, line)
return
}

output.write(result.outputType, result.formatted)
}
)
}

func testWithLog(with arguments: TestArguments) throws -> String {
Expand Down Expand Up @@ -96,9 +124,6 @@ final class BuildInteractorImpl: BuildInteractor {
let destination = try getDestination(for: platform, scheme: scheme, simulatorId: simulatorId)
buildArguments += ["-destination", destination]
}
if isQuiet {
buildArguments += ["-quiet"]
}
return buildArguments + arguments
}

Expand Down
5 changes: 1 addition & 4 deletions Sources/SwiftTools/Build/Domain/GetSimulatorIdUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ public protocol GetSimulatorIdUseCase {
final class GetSimulatorIdUseCaseImp: GetSimulatorIdUseCase {
private let shellService: ShellService
private let printService: PrintService
private let verboseController: VerboseController

init(shellService: ShellService, printService: PrintService, verboseController: VerboseController) {
init(shellService: ShellService, printService: PrintService) {
self.shellService = shellService
self.printService = printService
self.verboseController = verboseController
}

func callAsFunction(for platform: Platform, scheme: String) throws -> String {
Expand All @@ -20,7 +18,6 @@ final class GetSimulatorIdUseCaseImp: GetSimulatorIdUseCase {

private func getDeviceId(for keys: [String], scheme: String) throws -> String {
let destinations = try shellService.executeWithResult(arguments: ["xcodebuild", "-scheme", scheme, "-showdestinations", "-quiet"])
printService.printVerbose(destinations)
let components = destinations
.components(separatedBy: "\n")
let destination = try components.first(where: { isRowValid(keys: keys, row: $0) }) ?!+ "missing simulator"
Expand Down
139 changes: 72 additions & 67 deletions Sources/SwiftTools/Common/Shell/Platform/ShellService.swift
Original file line number Diff line number Diff line change
@@ -1,97 +1,102 @@
//
// File.swift
//
//
// Created by Kryštof Matěj on 05.01.2021.
//

import SwiftCLI
import XcbeautifyLib
import Foundation

public protocol ShellService {
func execute(arguments: [String]) throws
func executeWithVisibleOutput(arguments: [String]) throws
func executeWithResult(arguments: [String]) throws -> String
func executeWithXCBeautify(arguments: [String]) throws
func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws
}

final class ShellServiceImpl: ShellService {
nonisolated(unsafe) private var processes: [Process] = []

private func handleSignal(_ sig: Int32) {
for p in processes where p.isRunning {
kill(-p.processIdentifier, sig)
}

exit(sig)
}

final class ShellServiceImpl: ShellService, @unchecked Sendable {
private let printService: PrintService
private let verboseController: VerboseController

init(printService: PrintService, verboseController: VerboseController) {
init(printService: PrintService) {
self.printService = printService
self.verboseController = verboseController
self.setupSignalHandlers()
}

func execute(arguments: [String]) throws {
try executeTask(arguments: arguments, isOutputVisible: verboseController.isVerbose())
private func setupSignalHandlers() {
signal(SIGINT) { _ in
print("Killing self SIGINT, processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))")
handleSignal(SIGINT)
}

signal(SIGTERM) { _ in
print("Killing self SIGTERM, processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))")
handleSignal(SIGTERM)
}
}

func executeWithVisibleOutput(arguments: [String]) throws {
try executeTask(arguments: arguments, isOutputVisible: true)
func execute(arguments: [String]) throws {
try executeWithProcessing(arguments: arguments, onProcessLine: { line in
self.printService.printVerbose(line)
})
}

func executeWithResult(arguments: [String]) throws -> String {
return try executeTask(arguments: arguments, isOutputVisible: verboseController.isVerbose())
var output = ""
try executeWithProcessing(arguments: arguments, onProcessLine: { line in
self.printService.printVerbose(line)
output += line + "\n"
})
return output
}

@discardableResult private func executeTask(arguments: [String], isOutputVisible: Bool) throws -> String {
let output = CaptureStream()
let error = CaptureStream()
let outputStream = makeOutputStream(captureStream: output, isOutputVisible: isOutputVisible)

func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws {
let command = arguments.joined(separator: " ")
let task = Task(executable: "/bin/bash", arguments: ["-c", command], stdout: outputStream, stderr: error)
printService.printVerbose("shell command: '\(command)'")
let exitCode = task.runSync()

let outputString = output.readAll()

guard exitCode == 0 else {
let errorMessage = error.readAll()
printService.printText("Command output: \(outputString)")
printService.printText("Command error: \(errorMessage)")
let message = !errorMessage.isEmpty ? errorMessage : outputString
throw ToolsError(description: "shell command: '\(command)' failed with error: '\(message)'")
}
return outputString
}
let process = Process()
let pipe = Pipe()

private func makeOutputStream(captureStream: CaptureStream, isOutputVisible: Bool) -> WritableStream {
if isOutputVisible {
return SplitStream(streams: [captureStream, WriteStream.stdout])
} else {
return captureStream
}
}
printService.printVerbose("shell command: '\(command)'")
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = ["-c", command]
process.standardOutput = pipe
process.standardError = pipe

func executeWithXCBeautify(arguments: [String]) throws {
let printStream = WriteStream.stdout
let parser = XCBeautifier(
colored: true,
renderer: .terminal,
preserveUnbeautifiedLines: true,
additionalLines: { nil }
)
let outputStream = makeBeautifyStream(outputStream: printStream, parser: parser)
let handle = pipe.fileHandleForReading
var buffer = Data()

let command = arguments.joined(separator: " ")
let task = Task(executable: "/bin/bash", arguments: ["-c", command], stdout: outputStream)
let exitCode = task.runSync()

guard exitCode == 0 else {
try process.run()
addProcessToChildManagement(process: process)

while true {
let data = handle.availableData
if data.isEmpty { break }
buffer.append(data)

while let range = buffer.firstRange(of: Data([0x0A])) {
let lineData = buffer.subdata(in: 0..<range.lowerBound)
buffer.removeSubrange(0...range.lowerBound)

if let line = String(data: lineData, encoding: .utf8) {
onProcessLine(line)
}
}
}

process.waitUntilExit()

guard process.terminationStatus == 0 else {
throw ToolsError(description: "shell command: '\(command)' failed with error")
}
}

private func makeBeautifyStream(outputStream: WritableStream, parser: XCBeautifier) -> ProcessingStream {
return LineStream { line in
if let formatted = parser.format(line: line) {
outputStream.write(formatted + "\n")
} else {
outputStream.write(line + "\n")
}
private func addProcessToChildManagement(process: Process) {
processes.append(process)
printService.printVerbose("Added processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))")
process.terminationHandler = { process in
processes.removeAll { $0 === process }
self.printService.printVerbose("Removed processes:\(processes.map(\.processIdentifier).map({ "\($0)"}).joined(separator: ", "))")
}
}
}
27 changes: 8 additions & 19 deletions Tests/SwiftToolsTests/Build/Domain/BuildInteractorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,27 +40,22 @@ final class ShellServiceSpy: ShellService {
let arguments: [String]
}

struct ExecuteWithVisibleOutput {
let arguments: [String]
}

struct ExecuteWithResult {
let arguments: [String]
}

struct ExecuteWithXCBeautify {
struct ExecuteWithProcessing {
let arguments: [String]
let onProcessLine: (String) -> Void
}

var executeThrowBlock: (() throws -> Void)?
var executeWithVisibleOutputThrowBlock: (() throws -> Void)?
var executeWithResultThrowBlock: (() throws -> Void)?
var executeWithResultReturn: String
var executeWithXCBeautifyThrowBlock: (() throws -> Void)?
var executeWithProcessingThrowBlock: (() throws -> Void)?
var execute = [Execute]()
var executeWithVisibleOutput = [ExecuteWithVisibleOutput]()
var executeWithResult = [ExecuteWithResult]()
var executeWithXCBeautify = [ExecuteWithXCBeautify]()
var executeWithProcessing = [ExecuteWithProcessing]()

init(executeWithResultReturn: String) {
self.executeWithResultReturn = executeWithResultReturn
Expand All @@ -72,23 +67,17 @@ final class ShellServiceSpy: ShellService {
try executeThrowBlock?()
}

func executeWithVisibleOutput(arguments: [String]) throws {
let item = ExecuteWithVisibleOutput(arguments: arguments)
executeWithVisibleOutput.append(item)
try executeWithVisibleOutputThrowBlock?()
}

func executeWithResult(arguments: [String]) throws -> String {
let item = ExecuteWithResult(arguments: arguments)
executeWithResult.append(item)
try executeWithResultThrowBlock?()
return executeWithResultReturn
}

func executeWithXCBeautify(arguments: [String]) throws {
let item = ExecuteWithXCBeautify(arguments: arguments)
executeWithXCBeautify.append(item)
try executeWithXCBeautifyThrowBlock?()
func executeWithProcessing(arguments: [String], onProcessLine: @escaping (String) -> Void) throws {
let item = ExecuteWithProcessing(arguments: arguments, onProcessLine: onProcessLine)
executeWithProcessing.append(item)
try executeWithProcessingThrowBlock?()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ final class GetSimulatorIdUseCaseTests: XCTestCase {
sut = GetSimulatorIdUseCaseImp(
shellService: shellServiceSpy,
printService: printServiceSpy,
verboseController: verboseControllerSpy,
)
}

Expand Down
Loading