mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-08 04:20:07 +02:00
292 lines
10 KiB
Swift
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) })
|
|
}
|
|
}
|