1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 04:20:07 +02:00
Files
app/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift
2024-02-11 21:35:40 +01:00

292 lines
10 KiB
Swift

//
// PhpEnvironments.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpEnvironments {
// MARK: - Initializer
/**
Loads the currently active PHP installation upon startup. May be empty.
*/
init() {
self.currentInstall = ActivePhpInstallation.load()
}
/**
Creates the shared instance. Called when starting the app.
*/
static func prepare() {
_ = Self.shared
}
/**
Determine which PHP version the `php` formula is aliased to.
*/
@MainActor func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
PhpEnvironments.brewPhpAlias = self.homebrewPackage.version
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version).")
// Check if that version actually corresponds to an older version
let phpConfigExecutablePath = "\(Paths.optPath)/php/bin/php-config"
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"],
trimNewlines: false
).trimmingCharacters(in: .whitespacesAndNewlines)
if let version = try? VersionNumber.parse(longVersionString) {
PhpEnvironments.brewPhpAlias = version.short
if version.short != homebrewPackage.version {
Log.info("[BREW] An older version of `php` is actually installed (\(version.short)).")
}
} else {
Log.warn("Could not determine the actual version of the php binary; assuming Homebrew is correct.")
PhpEnvironments.brewPhpAlias = homebrewPackage.version
}
}
}
// MARK: - Properties
/** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate?
/** The static instance. Accessible at any time. */
static let shared = PhpEnvironments()
/** Whether the switcher is busy performing any actions. */
@MainActor var isBusy: Bool = false {
didSet {
MainMenu.shared.refreshIcon()
MainMenu.shared.rebuild()
}
}
/** All versions of PHP that are currently supported. */
var availablePhpVersions: [String] = []
/** All versions of PHP that are currently installed but not compatible. */
var incompatiblePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation? {
didSet {
// Let the PHP extension manager, if it exists, know the version changed
if let version = currentInstall?.version.short {
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
}
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
If you're up to date, `php` will be aliased to the latest version,
but that might not be the case since not everyone keeps their
software up-to-date.
As such, we take that information from Homebrew.
*/
static var brewPhpAlias: String = ""
/**
It's possible for the alias to be newer than the actual installed version of PHP.
*/
static var homebrewBrewPhpAlias: String {
if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" }
return PhpEnvironments.shared.homebrewPackage.version
}
/**
The currently linked and active PHP installation.
*/
static var phpInstall: ActivePhpInstallation? {
return Self.shared.currentInstall
}
/**
Information we were able to discern from the Homebrew info command.
*/
var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods
/**
The switcher that is currently in use.
This was originally added so the Internal and Valet switcher could be swapped out,
but currently this is no longer needed.
*/
public static var switcher: PhpSwitcher {
return InternalSwitcher()
}
/**
Alias that detects which versions of PHP are installed.
See also: `detectPhpVersions()`. Please note that this method
does *not* return the set of PHP versions that are supported.
*/
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
This step also detects which versions of PHP are incompatible with the current version of Valet.
If a PHP installation is currently broken, that will also be reflected.
Returns a `Set<String>` of installations that are considered valid.
*/
public func detectPhpVersions() async -> Set<String> {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
let supportedByValet: Set<String> = {
guard let version = Valet.shared.version else {
return Constants.DetectedPhpVersions
}
return Constants.ValetSupportedPhpVersionMatrix[version.major] ?? []
}()
var supportedVersions = Valet.installed ? versions.intersection(supportedByValet) : versions
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
let phpAliasInstall = PhpInstallation(phpAlias)
// Before inserting, ensure that the actual output matches the alias
// if that isn't the case, our formula remains out-of-date
if !phpAliasInstall.isMissingBinary {
supportedVersions.insert(phpAlias)
}
}
availablePhpVersions = Array(supportedVersions)
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
incompatiblePhpVersions = Array(versions.subtracting(supportedByValet))
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
Log.info("The PHP versions that were detected are: \(availablePhpVersions)")
Log.info("The PHP versions that were unsupported are: \(incompatiblePhpVersions)")
var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version)
}
cachedPhpInstallations = mappedVersions
return supportedVersions
}
/**
Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`.
If `generateHelpers` is set to true, after detecting
all versions, helper scripts are generated as well.
*/
public func extractPhpVersions(
from versions: [String],
checkBinaries: Bool = true,
generateHelpers: Bool = true
) async -> Set<String> {
let supported = Constants.DetectedPhpVersions
var output: Set<String> = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
return version.starts(with: "php@")
}.forEach { (string) in
let version = string.components(separatedBy: "php@")[1]
// Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& supported.contains(version)
&& (checkBinaries ? FileSystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.insert(version)
}
}
if generateHelpers {
for item in output {
await PhpHelper.generate(for: item)
}
}
return output
}
/**
Returns a list of `VersionNumber` instances based on the available PHP versions
that are valid to switch to for a given constraint.
*/
public func validVersions(for constraint: String) -> [VersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
.make(from: self.availablePhpVersions)
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
/**
Validates whether the currently running version matches the provided version.
*/
public func validate(_ version: String) -> Bool {
guard let install = PhpEnvironments.phpInstall else {
Log.info("It appears as if no PHP installation is currently active.")
return false
}
if install.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
return true
}
return false
}
/**
Returns the configuration file instance that is used for a specific config value.
You can then use the configuration file instance to change values.
*/
public func getConfigFile(forKey key: String) -> PhpConfigurationFile? {
guard let install = PhpEnvironments.phpInstall else {
return nil
}
return install.iniFiles
.reversed()
.first(where: { $0.has(key: key) })
}
}