diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 64aea7a..de4f628 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -69,6 +69,9 @@ C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetTest.swift */; }; + C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; }; + C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5635D276AB09000F12CCB /* VersionExtractor.swift */; }; + C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */; }; C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; }; C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */; }; C4B97B78275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */; }; @@ -171,6 +174,8 @@ C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigParserTest.swift; sourceTree = ""; }; C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = ""; }; C4AF9F7C275454A900D44ED0 /* ValetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetTest.swift; sourceTree = ""; }; + C4B5635D276AB09000F12CCB /* VersionExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionExtractor.swift; sourceTree = ""; }; + C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionExtractorTest.swift; sourceTree = ""; }; C4B97B74275CF08C003F3378 /* AppDelegate+MenuOutlets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+MenuOutlets.swift"; sourceTree = ""; }; C4B97B77275CF1B5003F3378 /* App+ActivationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+ActivationPolicy.swift"; sourceTree = ""; }; C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+GlobalHotkey.swift"; sourceTree = ""; }; @@ -336,6 +341,7 @@ C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */, C4CCBA6B275C567B008C7055 /* PMWindowController.swift */, 54AB03252763858F00A29D5F /* Timer.swift */, + C4B5635D276AB09000F12CCB /* VersionExtractor.swift */, ); path = Helpers; sourceTree = ""; @@ -396,6 +402,7 @@ C43A8A1925D9CD1000591B77 /* Utility.swift */, C4AF9F76275447F100D44ED0 /* ValetConfigParserTest.swift */, C4AF9F7C275454A900D44ED0 /* ValetTest.swift */, + C4B56360276AB0A500F12CCB /* VersionExtractorTest.swift */, ); path = "phpmon-tests"; sourceTree = ""; @@ -572,6 +579,7 @@ C476FF9822B0DD830098105B /* Alert.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */, + C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */, C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, @@ -599,6 +607,7 @@ C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, + C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */, C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */, @@ -621,6 +630,7 @@ C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */, C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */, C4AF9F7D275454A900D44ED0 /* ValetTest.swift in Sources */, + C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */, C4F780C525D80B75000DBC97 /* MenuBarImageGenerator.swift in Sources */, C4F780B725D80B5D000DBC97 /* App.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, @@ -792,7 +802,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = "4.1-rc1"; + MARKETING_VERSION = "4.1-rc2"; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -817,7 +827,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = "4.1-rc1"; + MARKETING_VERSION = "4.1-rc2"; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/README.md b/README.md index 3c6b218..44b3c2b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ PHP Monitor is a universal application that runs on Apple Silicon **and** Intel- * macOS 11 Big Sur or higher (supports macOS 12 Monterey) * Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` * The brew formula `php` has to be installed (which version is detected) -* Laravel Valet 2.13 or higher +* Laravel Valet 2.16.2 or higher (older versions might be compatible but are not supported) _You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._ diff --git a/phpmon-tests/VersionExtractorTest.swift b/phpmon-tests/VersionExtractorTest.swift new file mode 100644 index 0000000..38a7f28 --- /dev/null +++ b/phpmon-tests/VersionExtractorTest.swift @@ -0,0 +1,25 @@ +// +// VersionExtractorTest.swift +// phpmon-tests +// +// Created by Nico Verbruggen on 16/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import XCTest + +class VersionExtractorTest: XCTestCase { + + func testExtractVersion() { + XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1") + XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0") + } + + func testVersionComparison() { + XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending) + XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending) + XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame) + XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending) + } + +} diff --git a/phpmon/Constants.swift b/phpmon/Constants.swift index 483da6a..50f5c27 100644 --- a/phpmon/Constants.swift +++ b/phpmon/Constants.swift @@ -15,6 +15,16 @@ class Constants { */ static let LatestStablePhpVersion = "8.1" + /** + The minimum version of Valet that is recommended. + If the installed version is older, a notification will be shown + every time the app launches (with a recommendation to upgrade). + + The minimum requirement is currently synced to PHP 8.1 compatibility. + See also: https://github.com/laravel/valet/releases/tag/v2.16.2 + */ + static let MinimumRecommendedValetVersion = "2.16.2" + /** * The PHP versions supported by this application. * Versions that do not appear in this array are omitted from the list. diff --git a/phpmon/Domain/Extensions/StringExtension.swift b/phpmon/Domain/Extensions/StringExtension.swift index 55d94c8..78fec16 100644 --- a/phpmon/Domain/Extensions/StringExtension.swift +++ b/phpmon/Domain/Extensions/StringExtension.swift @@ -38,4 +38,37 @@ extension String { return String(self[start ..< end]) } + // Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/ + /* + <1> We split the version by period (.). + <2> Then, we find the difference of digit that we will zero pad. + <3> If there are no differences, we don't need to do anything and use simple .compare. + <4> We populate an array of missing zero. + <5> We add zero pad array to a version with a fewer period and zero. + <6> We user array components to build back our versions from components and compare them. + This time it will have the same period and number of digit. + */ + func versionCompare(_ otherVersion: String) -> ComparisonResult { + let versionDelimiter = "." + + var versionComponents = self.components(separatedBy: versionDelimiter) // <1> + var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter) + + let zeroDiff = versionComponents.count - otherVersionComponents.count // <2> + + if zeroDiff == 0 { // <3> + // Same format, compare normally + return self.compare(otherVersion, options: .numeric) + } else { + let zeros = Array(repeating: "0", count: abs(zeroDiff)) // <4> + if zeroDiff > 0 { + otherVersionComponents.append(contentsOf: zeros) // <5> + } else { + versionComponents.append(contentsOf: zeros) + } + return versionComponents.joined(separator: versionDelimiter) + .compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6> + } + } + } diff --git a/phpmon/Domain/Helpers/VersionExtractor.swift b/phpmon/Domain/Helpers/VersionExtractor.swift new file mode 100644 index 0000000..81bb05b --- /dev/null +++ b/phpmon/Domain/Helpers/VersionExtractor.swift @@ -0,0 +1,37 @@ +// +// VersionExtractor.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 16/12/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class VersionExtractor { + + public static func from(_ string: String) -> String? { + let regex = try! NSRegularExpression( + pattern: #"Laravel Valet (?(\d+)(.)(\d+)((.)(\d+))?)"#, + options: [] + ) + + let match = regex.matches( + in: string, + options: [], + range: NSMakeRange(0, string.count) + ).first + + guard let match = match else { + return nil + } + + let range = Range( + match.range(withName: "version"), + in: string + )! + + return String(string[range]) + } + +} diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index 9d4e658..18efb33 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -22,11 +22,7 @@ class Valet { var sites: [Site] = [] init() { - version = Actions.valet("--version") - .replacingOccurrences(of: "Laravel Valet ", with: "") - // TODO: Use regular expression to avoid deprecation notices - .split(separator: "\n").last? - .trimmingCharacters(in: .whitespacesAndNewlines) + version = VersionExtractor.from(Actions.valet("--version")) ?? "UNKNOWN" let file = FileManager.default.homeDirectoryForCurrentUser @@ -48,6 +44,22 @@ class Valet { resolvePaths(tld: config.tld) } + public func validateVersion() -> Void { + if version == "UNKNOWN" { + return print("The Valet version could not be extracted... that does not bode well.") + } + + if version.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending { + let version = version + print("Valet version \(version) is too old! (recommended: \(Constants.MinimumRecommendedValetVersion))") + DispatchQueue.main.async { + Alert.notify(message: "alert.min_valet_version.title".localized, info: "alert.min_valet_version.info".localized(version, Constants.MinimumRecommendedValetVersion)) + } + } else { + print("Valet version \(version) is recent enough, OK (recommended: \(Constants.MinimumRecommendedValetVersion))") + } + } + private func resolvePaths(tld: String) { sites = [] diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index ffafe1d..f832ebe 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -70,6 +70,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { // Attempt to find out more info about Valet print("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version)") + Valet.shared.validateVersion() print("PHP Monitor is ready to serve!") // Schedule a request to fetch the PHP version every 60 seconds diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index b38fbde..42050fc 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -150,6 +150,13 @@ directories, since PHP Monitor controls the services."; "alert.php_alias_conflict.title" = "Homebrew `php` formula alias conflict detected"; "alert.php_alias_conflict.info" = "PHP Monitor has detected conflicting `php` aliases in your Homebrew setup, both of which have been detected as installed.\n\nThis will likely result in failed linking when switching PHP versions, and will break PHP Monitor functionality.\n\nFor more information, please visit: https://github.com/nicoverbruggen/phpmon/issues/54"; +"alert.min_valet_version.title" = "Your version of Valet is outdated, please upgrade!"; +"alert.min_valet_version.info" = "You are currently running Valet %@. + +For optimal support of the latest versions of PHP and proper version switching, it is recommended that you update to version %@. + +You can do this by running `composer global update` in your terminal. After that, run `valet install` again. For best results, restart PHP Monitor after that."; + // STARTUP /// 1. PHP binary not found