~ Refactoring of install process

This commit is contained in:
David Bureš 2025-04-26 15:09:07 +02:00
parent 35eeb39096
commit 6e569376a1
11 changed files with 179 additions and 116 deletions

View File

@ -7,7 +7,17 @@
import Foundation import Foundation
enum PackageInstallationProcessSteps enum PackageInstallationProcessSteps: Equatable
{ {
case ready, searching, presentingSearchResults, installing, finished, fatalError, requiresSudoPassword, wrongArchitecture, binaryAlreadyExists, anotherProcessAlreadyRunning, installationTerminatedUnexpectedly case ready
case searching
case presentingSearchResults
case installing(packageToInstall: BrewPackage)
case finished
case fatalError(packageThatWasGettingInstalled: BrewPackage)
case requiresSudoPassword(packageThatWasGettingInstalled: BrewPackage)
case wrongArchitecture(packageThatWasGettingInstalled: BrewPackage)
case binaryAlreadyExists(packageThatWasGettingInstalled: BrewPackage)
case anotherProcessAlreadyRunning
case installationTerminatedUnexpectedly
} }

View File

@ -0,0 +1,13 @@
//
// Package Install Initialization Error.swift
// Cork
//
// Created by David Bureš - P on 22.04.2025.
//
import Foundation
enum PackageInstallationInitializationError: Error
{
case couldNotStartInstallProcessWithPackage(package: BrewPackage?)
}

View File

@ -10,8 +10,11 @@ import CorkShared
class InstallationProgressTracker: ObservableObject class InstallationProgressTracker: ObservableObject
{ {
@Published var packageBeingInstalled: PackageInProgressOfBeingInstalled = .init(package: .init(name: "", type: .formula, installedOn: nil, versions: [], sizeInBytes: 0, downloadCount: nil), installationStage: .downloadingCask, packageInstallationProgress: 0) @Published var installationStage: PackageInstallationStage = .downloadingCask
@Published var installationProgress: Double = 0
@Published var realTimeTerminalOutput: [RealTimeTerminalLine] = .init()
@Published var numberOfPackageDependencies: Int = 0 @Published var numberOfPackageDependencies: Int = 0
@Published var numberInLineOfPackageCurrentlyBeingFetched: Int = 0 @Published var numberInLineOfPackageCurrentlyBeingFetched: Int = 0
@Published var numberInLineOfPackageCurrentlyBeingInstalled: Int = 0 @Published var numberInLineOfPackageCurrentlyBeingInstalled: Int = 0
@ -38,30 +41,28 @@ class InstallationProgressTracker: ObservableObject
} }
@MainActor @MainActor
func installPackage(using brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws -> TerminalOutput func installPackage(packageToInstall: BrewPackage, using brewData: BrewDataStorage, cachedPackagesTracker: CachedPackagesTracker) async throws -> TerminalOutput
{ {
let package: BrewPackage = packageBeingInstalled.package AppConstants.shared.logger.debug("Installing package \(packageToInstall.name, privacy: .auto)")
AppConstants.shared.logger.debug("Installing package \(package.name, privacy: .auto)")
var installationResult: TerminalOutput = .init(standardOutput: "", standardError: "") var installationResult: TerminalOutput = .init(standardOutput: "", standardError: "")
if package.type == .formula if packageToInstall.type == .formula
{ {
AppConstants.shared.logger.info("Package \(package.name, privacy: .public) is Formula") AppConstants.shared.logger.info("Package \(packageToInstall.name, privacy: .public) is Formula")
let output: String = try await installFormula(using: brewData).joined(separator: "") let output: String = try await installFormula(packageToInstall).joined(separator: "")
installationResult.standardOutput.append(output) installationResult.standardOutput.append(output)
packageBeingInstalled.packageInstallationProgress = 10 installationProgress = 10
packageBeingInstalled.installationStage = .finished installationStage = .finished
} }
else else
{ {
AppConstants.shared.logger.info("Package is Cask") AppConstants.shared.logger.info("Package is Cask")
try await installCask(using: brewData) try await installCask(packageToInstall)
} }
do do
@ -77,17 +78,16 @@ class InstallationProgressTracker: ObservableObject
} }
@MainActor @MainActor
private func installFormula(using _: BrewDataStorage) async throws -> [String] private func installFormula(_ packageToInstall: BrewPackage) async throws -> [String]
{ {
let package: BrewPackage = packageBeingInstalled.package
var packageDependencies: [String] = .init() var packageDependencies: [String] = .init()
/// For some reason, the line `fetching [package name]` appears twice during the matching process, and the first one is a dud. Ignore that first one. /// For some reason, the line `fetching [package name]` appears twice during the matching process, and the first one is a dud. Ignore that first one.
var hasAlreadyMatchedLineAboutInstallingPackageItself: Bool = false var hasAlreadyMatchedLineAboutInstallingPackageItself: Bool = false
var installOutput: [String] = .init() var installOutput: [String] = .init()
AppConstants.shared.logger.info("Package \(package.name, privacy: .public) is Formula") AppConstants.shared.logger.info("Package \(packageToInstall.name, privacy: .public) is Formula")
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", package.name]) let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", packageToInstall.name])
installationProcess = process installationProcess = process
for await output in stream for await output in stream
{ {
@ -99,7 +99,7 @@ class InstallationProgressTracker: ObservableObject
if showRealTimeTerminalOutputs if showRealTimeTerminalOutputs
{ {
packageBeingInstalled.realTimeTerminalOutput.append(RealTimeTerminalLine(line: outputLine)) realTimeTerminalOutput.append(RealTimeTerminalLine(line: outputLine))
} }
AppConstants.shared.logger.info("Does the line contain an element from the array? \(outputLine.containsElementFromArray(packageDependencies), privacy: .public)") AppConstants.shared.logger.info("Does the line contain an element from the array? \(outputLine.containsElementFromArray(packageDependencies), privacy: .public)")
@ -107,7 +107,7 @@ class InstallationProgressTracker: ObservableObject
if outputLine.contains("Fetching dependencies") if outputLine.contains("Fetching dependencies")
{ {
// First, we have to get a list of all the dependencies // First, we have to get a list of all the dependencies
var matchedDependencies: String = try outputLine.regexMatch("(?<=\(package.name): ).*?(.*)") var matchedDependencies: String = try outputLine.regexMatch("(?<=\(packageToInstall.name): ).*?(.*)")
matchedDependencies = matchedDependencies.replacingOccurrences(of: " and", with: ",") // The last dependency is different, because it's preceded by "and" instead of "," so let's replace that "and" with "," so we can split it nicely matchedDependencies = matchedDependencies.replacingOccurrences(of: " and", with: ",") // The last dependency is different, because it's preceded by "and" instead of "," so let's replace that "and" with "," so we can split it nicely
AppConstants.shared.logger.debug("Matched Dependencies: \(matchedDependencies, privacy: .auto)") AppConstants.shared.logger.debug("Matched Dependencies: \(matchedDependencies, privacy: .auto)")
@ -120,43 +120,43 @@ class InstallationProgressTracker: ObservableObject
numberOfPackageDependencies = packageDependencies.count // Assign the number of dependencies to the tracker for the user to see numberOfPackageDependencies = packageDependencies.count // Assign the number of dependencies to the tracker for the user to see
packageBeingInstalled.packageInstallationProgress = 1 installationProgress = 1
} }
else if outputLine.contains("Installing dependencies") || outputLine.contains("Installing \(package.name) dependency") else if outputLine.contains("Installing dependencies") || outputLine.contains("Installing \(packageToInstall.name) dependency")
{ {
AppConstants.shared.logger.info("Will install dependencies!") AppConstants.shared.logger.info("Will install dependencies!")
packageBeingInstalled.installationStage = .installingDependencies installationStage = .installingDependencies
// Increment by 1 for each package that finished installing // Increment by 1 for each package that finished installing
numberInLineOfPackageCurrentlyBeingInstalled = numberInLineOfPackageCurrentlyBeingInstalled + 1 numberInLineOfPackageCurrentlyBeingInstalled = numberInLineOfPackageCurrentlyBeingInstalled + 1
AppConstants.shared.logger.info("Installing dependency \(self.numberInLineOfPackageCurrentlyBeingInstalled) of \(packageDependencies.count)") AppConstants.shared.logger.info("Installing dependency \(self.numberInLineOfPackageCurrentlyBeingInstalled) of \(packageDependencies.count)")
// TODO: Add a math formula for advancing the stepper // TODO: Add a math formula for advancing the stepper
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + Double(Double(10) / (Double(3) * Double(numberOfPackageDependencies))) installationProgress = installationProgress + Double(Double(10) / (Double(3) * Double(numberOfPackageDependencies)))
} }
else if outputLine.contains("Already downloaded") || (outputLine.contains("Fetching") && outputLine.containsElementFromArray(packageDependencies)) else if outputLine.contains("Already downloaded") || (outputLine.contains("Fetching") && outputLine.containsElementFromArray(packageDependencies))
{ {
AppConstants.shared.logger.info("Will fetch dependencies!") AppConstants.shared.logger.info("Will fetch dependencies!")
packageBeingInstalled.installationStage = .fetchingDependencies installationStage = .fetchingDependencies
numberInLineOfPackageCurrentlyBeingFetched = numberInLineOfPackageCurrentlyBeingFetched + 1 numberInLineOfPackageCurrentlyBeingFetched = numberInLineOfPackageCurrentlyBeingFetched + 1
AppConstants.shared.logger.info("Fetching dependency \(self.numberInLineOfPackageCurrentlyBeingFetched) of \(packageDependencies.count)") AppConstants.shared.logger.info("Fetching dependency \(self.numberInLineOfPackageCurrentlyBeingFetched) of \(packageDependencies.count)")
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + Double(Double(10) / (Double(3) * (Double(numberOfPackageDependencies) * Double(5)))) installationProgress = installationProgress + Double(Double(10) / (Double(3) * (Double(numberOfPackageDependencies) * Double(5))))
} }
else if outputLine.contains("Fetching \(package.name)") || outputLine.contains("Installing \(package.name)") else if outputLine.contains("Fetching \(packageToInstall.name)") || outputLine.contains("Installing \(packageToInstall.name)")
{ {
if hasAlreadyMatchedLineAboutInstallingPackageItself if hasAlreadyMatchedLineAboutInstallingPackageItself
{ /// Only the second line about the package being installed is valid { /// Only the second line about the package being installed is valid
AppConstants.shared.logger.info("Will install the package itself!") AppConstants.shared.logger.info("Will install the package itself!")
packageBeingInstalled.installationStage = .installingPackage installationStage = .installingPackage
// TODO: Add a math formula for advancing the stepper // TODO: Add a math formula for advancing the stepper
packageBeingInstalled.packageInstallationProgress = Double(packageBeingInstalled.packageInstallationProgress) + Double((Double(10) - Double(packageBeingInstalled.packageInstallationProgress)) / Double(2)) installationProgress = Double(installationProgress) + Double((Double(10) - Double(installationProgress)) / Double(2))
AppConstants.shared.logger.info("Stepper value: \(Double(Double(10) / (Double(3) * Double(self.numberOfPackageDependencies))))") AppConstants.shared.logger.info("Stepper value: \(Double(Double(10) / (Double(3) * Double(self.numberOfPackageDependencies))))")
} }
@ -164,47 +164,45 @@ class InstallationProgressTracker: ObservableObject
{ /// When it appears for the first time, ignore it { /// When it appears for the first time, ignore it
AppConstants.shared.logger.info("Matched the dud line about the package itself being installed!") AppConstants.shared.logger.info("Matched the dud line about the package itself being installed!")
hasAlreadyMatchedLineAboutInstallingPackageItself = true hasAlreadyMatchedLineAboutInstallingPackageItself = true
packageBeingInstalled.packageInstallationProgress = Double(packageBeingInstalled.packageInstallationProgress) + Double((Double(10) - Double(packageBeingInstalled.packageInstallationProgress)) / Double(2)) installationProgress = Double(installationProgress) + Double((Double(10) - Double(installationProgress)) / Double(2))
} }
} }
installOutput.append(outputLine) installOutput.append(outputLine)
AppConstants.shared.logger.debug("Current installation stage: \(self.packageBeingInstalled.installationStage.description, privacy: .public)") AppConstants.shared.logger.debug("Current installation stage: \(self.installationStage.description, privacy: .public)")
case .standardError(let errorLine): case .standardError(let errorLine):
AppConstants.shared.logger.error("Errored out: \(errorLine, privacy: .public)") AppConstants.shared.logger.error("Errored out: \(errorLine, privacy: .public)")
if showRealTimeTerminalOutputs if showRealTimeTerminalOutputs
{ {
packageBeingInstalled.realTimeTerminalOutput.append(RealTimeTerminalLine(line: errorLine)) realTimeTerminalOutput.append(RealTimeTerminalLine(line: errorLine))
} }
if errorLine.contains("a password is required") if errorLine.contains("a password is required")
{ {
AppConstants.shared.logger.warning("Install requires sudo") AppConstants.shared.logger.warning("Install requires sudo")
packageBeingInstalled.installationStage = .requiresSudoPassword installationStage = .requiresSudoPassword
} }
} }
} }
packageBeingInstalled.packageInstallationProgress = 10 installationProgress = 10
packageBeingInstalled.installationStage = .finished installationStage = .finished
return installOutput return installOutput
} }
@MainActor @MainActor
func installCask(using _: BrewDataStorage) async throws func installCask(_ packageToInstall: BrewPackage) async throws
{ {
let package: BrewPackage = packageBeingInstalled.package
AppConstants.shared.logger.info("Package is Cask") AppConstants.shared.logger.info("Package is Cask")
AppConstants.shared.logger.debug("Installing package \(package.name, privacy: .public)") AppConstants.shared.logger.debug("Installing package \(packageToInstall.name, privacy: .public)")
let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", package.name]) let (stream, process): (AsyncStream<StreamedTerminalOutput>, Process) = shell(AppConstants.shared.brewExecutablePath, ["install", "--no-quarantine", packageToInstall.name])
installationProcess = process installationProcess = process
for await output in stream for await output in stream
{ {
@ -215,56 +213,56 @@ class InstallationProgressTracker: ObservableObject
if showRealTimeTerminalOutputs if showRealTimeTerminalOutputs
{ {
packageBeingInstalled.realTimeTerminalOutput.append(RealTimeTerminalLine(line: outputLine)) realTimeTerminalOutput.append(RealTimeTerminalLine(line: outputLine))
} }
if outputLine.contains("Downloading") if outputLine.contains("Downloading")
{ {
AppConstants.shared.logger.info("Will download Cask") AppConstants.shared.logger.info("Will download Cask")
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 2 installationProgress = installationProgress + 2
packageBeingInstalled.installationStage = .downloadingCask installationStage = .downloadingCask
} }
else if outputLine.contains("Installing Cask") else if outputLine.contains("Installing Cask")
{ {
AppConstants.shared.logger.info("Will install Cask") AppConstants.shared.logger.info("Will install Cask")
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 2 installationProgress = installationProgress + 2
packageBeingInstalled.installationStage = .installingCask installationStage = .installingCask
} }
else if outputLine.contains("Moving App") else if outputLine.contains("Moving App")
{ {
AppConstants.shared.logger.info("Moving App") AppConstants.shared.logger.info("Moving App")
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 2 installationProgress = installationProgress + 2
packageBeingInstalled.installationStage = .movingCask installationStage = .movingCask
} }
else if outputLine.contains("Linking binary") else if outputLine.contains("Linking binary")
{ {
AppConstants.shared.logger.info("Linking Binary") AppConstants.shared.logger.info("Linking Binary")
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 2 installationProgress = installationProgress + 2
packageBeingInstalled.installationStage = .linkingCaskBinary installationStage = .linkingCaskBinary
} }
else if outputLine.contains("Purging files") else if outputLine.contains("Purging files")
{ {
AppConstants.shared.logger.info("Purging old version of cask \(package.name)") AppConstants.shared.logger.info("Purging old version of cask \(packageToInstall.name)")
packageBeingInstalled.installationStage = .installingCask installationStage = .installingCask
packageBeingInstalled.packageInstallationProgress = packageBeingInstalled.packageInstallationProgress + 1 installationProgress = installationProgress + 1
} }
else if outputLine.contains("was successfully installed") else if outputLine.contains("was successfully installed")
{ {
AppConstants.shared.logger.info("Finished installing app") AppConstants.shared.logger.info("Finished installing app")
packageBeingInstalled.installationStage = .finished installationStage = .finished
packageBeingInstalled.packageInstallationProgress = 10 installationProgress = 10
} }
case .standardError(let errorLine): case .standardError(let errorLine):
@ -272,26 +270,26 @@ class InstallationProgressTracker: ObservableObject
if showRealTimeTerminalOutputs if showRealTimeTerminalOutputs
{ {
packageBeingInstalled.realTimeTerminalOutput.append(RealTimeTerminalLine(line: errorLine)) realTimeTerminalOutput.append(RealTimeTerminalLine(line: errorLine))
} }
if errorLine.contains("a password is required") if errorLine.contains("a password is required")
{ {
AppConstants.shared.logger.warning("Install requires sudo") AppConstants.shared.logger.warning("Install requires sudo")
packageBeingInstalled.installationStage = .requiresSudoPassword installationStage = .requiresSudoPassword
} }
else if errorLine.contains("there is already an App at") else if errorLine.contains("there is already an App at")
{ {
AppConstants.shared.logger.warning("The app already exists") AppConstants.shared.logger.warning("The app already exists")
packageBeingInstalled.installationStage = .binaryAlreadyExists installationStage = .binaryAlreadyExists
} }
else if errorLine.contains(/depends on hardware architecture being.+but you are running/) else if errorLine.contains(/depends on hardware architecture being.+but you are running/)
{ {
AppConstants.shared.logger.warning("Package is wrong architecture") AppConstants.shared.logger.warning("Package is wrong architecture")
packageBeingInstalled.installationStage = .wrongArchitecture installationStage = .wrongArchitecture
} }
} }
} }

View File

@ -5,10 +5,10 @@
// Created by David Bureš on 03.07.2022. // Created by David Bureš on 03.07.2022.
// //
import ButtonKit
import CorkNotifications import CorkNotifications
import CorkShared import CorkShared
import SwiftUI import SwiftUI
import ButtonKit
struct AddFormulaView: View struct AddFormulaView: View
{ {
@ -39,10 +39,35 @@ struct AddFormulaView: View
{ {
[.ready, .presentingSearchResults].contains(packageInstallationProcessStep) [.ready, .presentingSearchResults].contains(packageInstallationProcessStep)
} }
var isDismissable: Bool var isDismissable: Bool
{ {
[.ready, .presentingSearchResults, .fatalError, .anotherProcessAlreadyRunning, .binaryAlreadyExists, .requiresSudoPassword, .wrongArchitecture, .anotherProcessAlreadyRunning, .installationTerminatedUnexpectedly, .installing].contains(packageInstallationProcessStep) if case .installing = packageInstallationProcessStep
{
return true
}
if case .binaryAlreadyExists = packageInstallationProcessStep
{
return true
}
if case .fatalError = packageInstallationProcessStep
{
return true
}
if case .wrongArchitecture = packageInstallationProcessStep
{
return true
}
if case .requiresSudoPassword = packageInstallationProcessStep
{
return true
}
return [.ready, .presentingSearchResults, .anotherProcessAlreadyRunning, .anotherProcessAlreadyRunning, .installationTerminatedUnexpectedly].contains(packageInstallationProcessStep)
} }
var sheetTitle: LocalizedStringKey var sheetTitle: LocalizedStringKey
@ -109,32 +134,40 @@ struct AddFormulaView: View
installationProgressTracker: installationProgressTracker installationProgressTracker: installationProgressTracker
) )
case .installing: case .installing(let packageToInstall):
InstallingPackageView( InstallingPackageView(
installationProgressTracker: installationProgressTracker, installationProgressTracker: installationProgressTracker,
packageToInstall: packageToInstall,
packageInstallationProcessStep: $packageInstallationProcessStep packageInstallationProcessStep: $packageInstallationProcessStep
) )
case .finished: case .finished:
InstallationFinishedSuccessfullyView() InstallationFinishedSuccessfullyView()
case .fatalError: /// This shows up when the function for executing the install action throws an error case .fatalError(let packageThatWasGettingInstalled): /// This shows up when the function for executing the install action throws an error
InstallationFatalErrorView(installationProgressTracker: installationProgressTracker) InstallationFatalErrorView(
installationProgressTracker: installationProgressTracker,
packageThatWasGettingInstalled: packageThatWasGettingInstalled
)
case .requiresSudoPassword: case .requiresSudoPassword(let packageThatWasGettingInstalled):
SudoRequiredView(installationProgressTracker: installationProgressTracker) SudoRequiredView(
packageThatWasGettingInstalled: packageThatWasGettingInstalled
)
case .wrongArchitecture: case .wrongArchitecture(let packageThatWasGettingInstalled):
WrongArchitectureView(installationProgressTracker: installationProgressTracker) WrongArchitectureView(
packageThatWasGettingInstalled: packageThatWasGettingInstalled
)
case .binaryAlreadyExists: case .binaryAlreadyExists(let packageThatWasGettingInstalled):
BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker) BinaryAlreadyExistsView(installationProgressTracker: installationProgressTracker, packageThatWasGettingInstalled: packageThatWasGettingInstalled)
case .anotherProcessAlreadyRunning: case .anotherProcessAlreadyRunning:
AnotherProcessAlreadyRunningView() AnotherProcessAlreadyRunningView()
case .installationTerminatedUnexpectedly: case .installationTerminatedUnexpectedly:
InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput) InstallationTerminatedUnexpectedlyView(terminalOutputOfTheInstallation: installationProgressTracker.realTimeTerminalOutput)
} }
} }
.navigationTitle(sheetTitle) .navigationTitle(sheetTitle)
@ -148,7 +181,7 @@ struct AddFormulaView: View
{ {
dismiss() dismiss()
installationProgressTracker.cancel() installationProgressTracker.cancel()
do do
{ {
try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker) try await brewData.synchronizeInstalledPackages(cachedPackagesTracker: cachedDownloadsTracker)

View File

@ -125,11 +125,10 @@ struct InstallationInitialView: View
return return
} }
installationProgressTracker.packageBeingInstalled = PackageInProgressOfBeingInstalled(package: packageToInstall, installationStage: .ready, packageInstallationProgress: 0)
AppConstants.shared.logger.debug("Packages to install: \(installationProgressTracker.packageBeingInstalled.package.name, privacy: .public)") AppConstants.shared.logger.debug("Packages to install: \(packageToInstall.name, privacy: .public)")
packageInstallationProcessStep = .installing packageInstallationProcessStep = .installing(packageToInstall: packageToInstall)
} label: { } label: {
Text("add-package.install.action") Text("add-package.install.action")

View File

@ -5,8 +5,8 @@
// Created by David Bureš on 29.09.2023. // Created by David Bureš on 29.09.2023.
// //
import SwiftUI
import CorkShared import CorkShared
import SwiftUI
struct InstallingPackageView: View struct InstallingPackageView: View
{ {
@ -14,11 +14,13 @@ struct InstallingPackageView: View
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage @EnvironmentObject var brewData: BrewDataStorage
@EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker @EnvironmentObject var cachedPackagesTracker: CachedPackagesTracker
@ObservedObject var installationProgressTracker: InstallationProgressTracker @ObservedObject var installationProgressTracker: InstallationProgressTracker
let packageToInstall: BrewPackage
@Binding var packageInstallationProcessStep: PackageInstallationProcessSteps @Binding var packageInstallationProcessStep: PackageInstallationProcessSteps
@State var isShowingRealTimeOutput: Bool = false @State var isShowingRealTimeOutput: Bool = false
@ -27,13 +29,13 @@ struct InstallingPackageView: View
{ {
VStack(alignment: .leading) VStack(alignment: .leading)
{ {
if installationProgressTracker.packageBeingInstalled.installationStage != .finished if installationProgressTracker.installationStage != .finished
{ {
ProgressView(value: installationProgressTracker.packageBeingInstalled.packageInstallationProgress, total: 10) ProgressView(value: installationProgressTracker.installationProgress, total: 10)
{ {
VStack(alignment: .leading) VStack(alignment: .leading)
{ {
switch installationProgressTracker.packageBeingInstalled.installationStage switch installationProgressTracker.installationStage
{ {
case .ready: case .ready:
Text("add-package.install.ready") Text("add-package.install.ready")
@ -56,36 +58,36 @@ struct InstallingPackageView: View
// CASKS // CASKS
case .downloadingCask: case .downloadingCask:
Text("add-package.install.downloading-cask-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.downloading-cask-\(packageToInstall.name)")
case .installingCask: case .installingCask:
Text("add-package.install.installing-cask-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.installing-cask-\(packageToInstall.name)")
case .linkingCaskBinary: case .linkingCaskBinary:
Text("add-package.install.linking-cask-binary") Text("add-package.install.linking-cask-binary")
case .movingCask: case .movingCask:
Text("add-package.install.moving-cask-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.moving-cask-\(packageToInstall.name)")
case .requiresSudoPassword: case .requiresSudoPassword:
Text("add-package.install.requires-sudo-password-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.requires-sudo-password-\(packageToInstall.name)")
.onAppear .onAppear
{ {
packageInstallationProcessStep = .requiresSudoPassword packageInstallationProcessStep = .requiresSudoPassword(packageThatWasGettingInstalled: packageToInstall)
} }
case .wrongArchitecture: case .wrongArchitecture:
Text("add-package.install.wrong-architecture.title") Text("add-package.install.wrong-architecture.title")
.onAppear .onAppear
{ {
packageInstallationProcessStep = .wrongArchitecture packageInstallationProcessStep = .wrongArchitecture(packageThatWasGettingInstalled: packageToInstall)
} }
case .binaryAlreadyExists: case .binaryAlreadyExists:
Text("add-package.install.binary-already-exists-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.binary-already-exists-\(packageToInstall.name)")
.onAppear .onAppear
{ {
packageInstallationProcessStep = .binaryAlreadyExists packageInstallationProcessStep = .binaryAlreadyExists(packageThatWasGettingInstalled: packageToInstall)
} }
case .terminatedUnexpectedly: case .terminatedUnexpectedly:
@ -96,7 +98,7 @@ struct InstallingPackageView: View
} }
} }
LiveTerminalOutputView( LiveTerminalOutputView(
lineArray: $installationProgressTracker.packageBeingInstalled.realTimeTerminalOutput, lineArray: $installationProgressTracker.realTimeTerminalOutput,
isRealTimeTerminalOutputExpanded: $isShowingRealTimeOutput isRealTimeTerminalOutputExpanded: $isShowingRealTimeOutput
) )
} }
@ -117,19 +119,19 @@ struct InstallingPackageView: View
do do
{ {
let installationResult: TerminalOutput = try await installationProgressTracker.installPackage( let installationResult: TerminalOutput = try await installationProgressTracker.installPackage(
using: brewData, packageToInstall: packageToInstall, using: brewData,
cachedPackagesTracker: cachedPackagesTracker cachedPackagesTracker: cachedPackagesTracker
) )
AppConstants.shared.logger.debug("Installation result:\nStandard output: \(installationResult.standardOutput, privacy: .public)\nStandard error: \(installationResult.standardError, privacy: .public)") AppConstants.shared.logger.debug("Installation result:\nStandard output: \(installationResult.standardOutput, privacy: .public)\nStandard error: \(installationResult.standardError, privacy: .public)")
/// Check if the package installation stag at the end of the install process was something unexpected. Normal package installations go through multiple steps, and the three listed below are not supposed to be the end state. This means that something went wrong during the installation /// Check if the package installation stag at the end of the install process was something unexpected. Normal package installations go through multiple steps, and the three listed below are not supposed to be the end state. This means that something went wrong during the installation
let installationStage: PackageInstallationStage = installationProgressTracker.packageBeingInstalled.installationStage let installationStage: PackageInstallationStage = installationProgressTracker.installationStage
if [.installingCask, .installingPackage, .ready].contains(installationStage) if [.installingCask, .installingPackage, .ready].contains(installationStage)
{ {
AppConstants.shared.logger.warning("The installation process quit before it was supposed to") AppConstants.shared.logger.warning("The installation process quit before it was supposed to")
installationProgressTracker.packageBeingInstalled.installationStage = .terminatedUnexpectedly installationProgressTracker.installationStage = .terminatedUnexpectedly
} }
} }
catch let fatalInstallationError catch let fatalInstallationError

View File

@ -7,6 +7,7 @@
import CorkShared import CorkShared
import SwiftUI import SwiftUI
import ButtonKit
struct PresentingSearchResultsView: View struct PresentingSearchResultsView: View
{ {
@ -149,12 +150,19 @@ struct PresentingSearchResultsView: View
@ViewBuilder @ViewBuilder
var startInstallProcessButton: some View var startInstallProcessButton: some View
{ {
Button // This has to be an AsyncButton so it shakes
AsyncButton
{ {
packageInstallationProcessStep = .installing guard let packageToInstall = foundPackageSelection else
{
throw PackageInstallationInitializationError.couldNotStartInstallProcessWithPackage(package: nil)
}
packageInstallationProcessStep = .installing(packageToInstall: packageToInstall)
} label: { } label: {
Text("add-package.install.action") Text("add-package.install.action")
} }
.throwableButtonStyle(.shake)
.keyboardShortcut(.defaultAction) .keyboardShortcut(.defaultAction)
.disabled(foundPackageSelection == nil) .disabled(foundPackageSelection == nil)
} }

View File

@ -16,6 +16,8 @@ struct BinaryAlreadyExistsView: View, Sendable
@EnvironmentObject var brewData: BrewDataStorage @EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker @ObservedObject var installationProgressTracker: InstallationProgressTracker
let packageThatWasGettingInstalled: BrewPackage
var body: some View var body: some View
{ {
@ -24,7 +26,7 @@ struct BinaryAlreadyExistsView: View, Sendable
VStack(alignment: .leading, spacing: 10) VStack(alignment: .leading, spacing: 10)
{ {
HeadlineWithSubheadline( HeadlineWithSubheadline(
headline: "add-package.install.binary-already-exists-\(installationProgressTracker.packageBeingInstalled.package.name)", headline: "add-package.install.binary-already-exists-\(packageThatWasGettingInstalled.name)",
subheadline: "add-package.install.binary-already-exists.subheadline", subheadline: "add-package.install.binary-already-exists.subheadline",
alignment: .leading alignment: .leading
) )

View File

@ -11,12 +11,14 @@ struct InstallationFatalErrorView: View
{ {
@ObservedObject var installationProgressTracker: InstallationProgressTracker @ObservedObject var installationProgressTracker: InstallationProgressTracker
let packageThatWasGettingInstalled: BrewPackage
var body: some View var body: some View
{ {
ComplexWithIcon(systemName: "exclamationmark.triangle") ComplexWithIcon(systemName: "exclamationmark.triangle")
{ {
HeadlineWithSubheadline( HeadlineWithSubheadline(
headline: "add-package.fatal-error-\(installationProgressTracker.packageBeingInstalled.package.name)", headline: "add-package.fatal-error-\(packageThatWasGettingInstalled.name)",
subheadline: "add-package.fatal-error.description", subheadline: "add-package.fatal-error.description",
alignment: .leading alignment: .leading
) )

View File

@ -14,7 +14,7 @@ struct SudoRequiredView: View, Sendable
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage @EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker let packageThatWasGettingInstalled: BrewPackage
var body: some View var body: some View
{ {
@ -24,14 +24,14 @@ struct SudoRequiredView: View, Sendable
{ {
VStack(alignment: .leading, spacing: 10) VStack(alignment: .leading, spacing: 10)
{ {
Text("add-package.install.requires-sudo-password-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add-package.install.requires-sudo-password-\(packageThatWasGettingInstalled.name)")
.font(.headline) .font(.headline)
ManualInstallInstructions(installationProgressTracker: installationProgressTracker) manualInstallInstructions
} }
} }
Text("add.package.install.requires-sudo-password.terminal-instructions-\(installationProgressTracker.packageBeingInstalled.package.name)") Text("add.package.install.requires-sudo-password.terminal-instructions-\(packageThatWasGettingInstalled.name)")
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
@ -51,19 +51,15 @@ struct SudoRequiredView: View, Sendable
} }
.fixedSize() .fixedSize()
} }
}
@ViewBuilder
private struct ManualInstallInstructions: View var manualInstallInstructions: some View
{
let installationProgressTracker: InstallationProgressTracker
var manualInstallCommand: String
{
return "brew install \(installationProgressTracker.packageBeingInstalled.package.type == .cask ? "--cask" : "") \(installationProgressTracker.packageBeingInstalled.package.name)"
}
var body: some View
{ {
var manualInstallCommand: String
{
return "brew install \(packageThatWasGettingInstalled.type == .cask ? "--cask" : "") \(packageThatWasGettingInstalled.name)"
}
VStack VStack
{ {
Text("add-package.install.requires-sudo-password.description") Text("add-package.install.requires-sudo-password.description")

View File

@ -13,8 +13,8 @@ struct WrongArchitectureView: View, Sendable
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@EnvironmentObject var brewData: BrewDataStorage @EnvironmentObject var brewData: BrewDataStorage
@ObservedObject var installationProgressTracker: InstallationProgressTracker let packageThatWasGettingInstalled: BrewPackage
var body: some View var body: some View
{ {
@ -24,7 +24,7 @@ struct WrongArchitectureView: View, Sendable
{ {
HeadlineWithSubheadline( HeadlineWithSubheadline(
headline: "add-package.install.wrong-architecture.title", headline: "add-package.install.wrong-architecture.title",
subheadline: "add-package.install.wrong-architecture-\(installationProgressTracker.packageBeingInstalled.package.name).user-architecture-is-\(ProcessInfo().CPUArchitecture == .arm ? "Apple Silicon" : "Intel")", subheadline: "add-package.install.wrong-architecture-\(packageThatWasGettingInstalled.name).user-architecture-is-\(ProcessInfo().CPUArchitecture == .arm ? "Apple Silicon" : "Intel")",
alignment: .leading alignment: .leading
) )
} }