1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2026-04-06 19:10:08 +02:00
Files
app/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift
2025-10-09 15:09:42 +02:00

141 lines
4.6 KiB
Swift

//
// BrewCommand.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/03/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol BrewCommand {
func execute(shell: ShellProtocol, onProgress: @escaping (BrewCommandProgress) -> Void) async throws
func getCommandTitle() -> String
}
extension BrewCommand {
internal func reportInstallationProgress(_ text: String) -> (Double, String)? {
// Special cases: downloading a manifest is effectively fetching metadata
if text.contains("==> Downloading") && text.contains("/manifests/") {
return (0.1, "phpman.steps.fetching".localized)
}
// Logical progress evaluation (reverse order for accuracy)
if text.contains("==> Summary") {
return (0.90, "phpman.steps.summary".localized)
}
if text.contains("==> Pouring") {
if let subject = extractContext(from: text) {
return (0.80, "phpman.steps.pouring".localized + "\n(\(subject))")
}
return (0.80, "phpman.steps.pouring".localized)
}
if text.contains("==> Installing") {
if let subject = extractContext(from: text) {
return (0.60, "phpman.steps.installing".localized + "\n(\(subject))")
}
return (0.60, "phpman.steps.installing".localized)
}
if text.contains("==> Downloading") {
if let subject = extractContext(from: text) {
return (0.25, "phpman.steps.downloading".localized + "\n(\(subject))")
}
return (0.25, "phpman.steps.downloading".localized)
}
if text.contains("==> Fetching") {
return (0.1, "phpman.steps.fetching".localized)
}
return nil
}
internal func extractContext(from text: String) -> String? {
var pattern = #""#
if text.contains("==> Fetching") {
pattern = #"==> Fetching (\S+)"#
}
if text.contains("==> Downloading") {
pattern = #"==> Downloading (\S+)"#
}
if text.contains("==> Installing") {
pattern = #"==> Installing (\S+)"#
}
if text.contains("==> Pouring") {
pattern = #"==> Pouring (\S+)"#
}
guard let regex = try? NSRegularExpression(pattern: pattern) else {
return nil
}
let range = NSRange(text.startIndex..<text.endIndex, in: text)
if let match = regex.firstMatch(in: text, options: [], range: range) {
if let formulaRange = Range(match.range(at: 1), in: text) {
return String(text[formulaRange])
}
}
return nil
}
internal func run(
shell: ShellProtocol,
_ command: String,
_ onProgress: @escaping (BrewCommandProgress) -> Void
) async throws {
var loggedMessages: [String] = []
let (process, _) = try! await shell.attach(
command,
didReceiveOutput: { text, _ in
if !text.isEmpty {
Log.perf(text)
loggedMessages.append(text)
}
if let (number, text) = self.reportInstallationProgress(text) {
onProgress(.create(value: number, title: getCommandTitle(), description: text))
}
},
withTimeout: .minutes(15)
)
if process.terminationStatus <= 0 {
loggedMessages = []
return
} else {
throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages)
}
}
internal func checkPhpTap(
shell: ShellProtocol,
_ onProgress: @escaping (BrewCommandProgress) -> Void
) async throws {
if !BrewDiagnostics.shared.installedTaps.contains("shivammathur/php") {
let command = "brew tap shivammathur/php"
try await run(shell: shell, command, onProgress)
}
if !BrewDiagnostics.shared.installedTaps.contains("shivammathur/extensions") {
let command = "brew tap shivammathur/extensions"
try await run(shell: shell, command, onProgress)
}
}
}
struct BrewCommandProgress {
let value: Double
let title: String
let description: String
public static func create(value: Double, title: String, description: String) -> BrewCommandProgress {
return BrewCommandProgress(value: value, title: title, description: description)
}
}
struct BrewCommandError: Error {
let error: String
let log: [String]
}