diff --git a/.gitignore b/.gitignore index 484172f..05e199d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ phpmon.xcodeproj/project.xcworkspace phpmon.xcodeproj/xcuserdata PHP Monitor.xcodeproj/project.xcworkspace PHP Monitor.xcodeproj/xcuserdata -phpmon-updater/PHP Monitor Self-Updater.app -.DS_Store \ No newline at end of file +phpmon-updater/PHP Monitor Self-Updater.app/ +.DS_Store diff --git a/DEVELOPER.md b/DEVELOPER.md index ff43b26..3518e67 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -28,15 +28,22 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd build button in Xcode +### PHP Monitor + If you'd like to build PHP Monitor yourself, you need: * Xcode (usually the latest version) +* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor) * The contents of this repository -Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to immediately build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.) +Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.) If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive. +### PHP Monitor Updater + +Select the separate target and build. You can then copy the product to the `phpmon-updater` directory. The binary will be re-signed when distributing the main build. + ## 🚀 Release procedure 1. Merge into `main` diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index b33939a..a1f46bd 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 03E36FE728D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; }; 03E36FE828D9219000636F7F /* ActiveShell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E36FE628D9219000636F7F /* ActiveShell.swift */; }; - 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; }; + 5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; }; 5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; 5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; }; 5489625928313231004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; }; @@ -49,25 +49,48 @@ C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */; }; C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; }; C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */; }; + C406A5F3298AD2CE00B5B85A /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C406A5F2298AD2CE00B5B85A /* main.swift */; }; + C406A5F7298AD2CF00B5B85A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C406A5F6298AD2CF00B5B85A /* Assets.xcassets */; }; + C406A602298AD50D00B5B85A /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C406A601298AD50D00B5B85A /* Updater.swift */; }; C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; }; C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */; }; C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; }; C4080FFB27BD956700BF2C6B /* BetterAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */; }; + C409349D298EE8E900D25014 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C409349C298EE8E900D25014 /* AppUpdater.swift */; }; + C409349E298EE8E900D25014 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C409349C298EE8E900D25014 /* AppUpdater.swift */; }; + C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C409349C298EE8E900D25014 /* AppUpdater.swift */; }; + C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C409349C298EE8E900D25014 /* AppUpdater.swift */; }; + C40934A2298EEB2C00D25014 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40934A1298EEB2C00D25014 /* CaskFile.swift */; }; + C40934A3298EEB2C00D25014 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40934A1298EEB2C00D25014 /* CaskFile.swift */; }; + C40934A4298EEB2C00D25014 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40934A1298EEB2C00D25014 /* CaskFile.swift */; }; + C40934A5298EEB2C00D25014 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40934A1298EEB2C00D25014 /* CaskFile.swift */; }; + C40934A7298EEB8700D25014 /* phpmon-dev.rb in Resources */ = {isa = PBXBuildFile; fileRef = C40934A6298EEB8700D25014 /* phpmon-dev.rb */; }; + C40934A8298EEB8700D25014 /* phpmon-dev.rb in Resources */ = {isa = PBXBuildFile; fileRef = C40934A6298EEB8700D25014 /* phpmon-dev.rb */; }; + C40934A9298EEB8700D25014 /* phpmon-dev.rb in Resources */ = {isa = PBXBuildFile; fileRef = C40934A6298EEB8700D25014 /* phpmon-dev.rb */; }; + C40934AB298EEDA900D25014 /* CaskFileParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */; }; C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; C40C5C9C2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C40C5C9D2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; - C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; - C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; - C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; - C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; + C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */; }; + C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */; }; + C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; + C40C7F2927721FF600DDDCDC /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; }; C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2F27722E8D00DDDCDC /* Logger.swift */; }; + C40D725A2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */; }; + C40D725B2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */; }; + C40D725C2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */; }; + C40D725D2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */; }; + C40D725F2A018AE30054A067 /* BrewFormulaUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */; }; + C40D72602A018AE30054A067 /* BrewFormulaUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */; }; + C40D72612A018AE30054A067 /* BrewFormulaUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */; }; + C40D72622A018AE30054A067 /* BrewFormulaUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */; }; C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40F505428ECA64E004AD45B /* TestableConfigurations.swift */; }; C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; }; C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; }; C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */; }; - C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C412E5FC25700D5300A1FB67 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; }; C413E43528DA3EB100AE33C7 /* TestableShellTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C413E43428DA3EB100AE33C7 /* TestableShellTest.swift */; }; C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; @@ -79,6 +102,10 @@ C417DC74277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; }; C417DC75277614690015E6EE /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C417DC73277614690015E6EE /* Helpers.swift */; }; C4181F1128FAF9330042EA28 /* UITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4181F1028FAF9330042EA28 /* UITestCase.swift */; }; + C41ADCE82970CCC700120423 /* FSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41ADCE72970CCC700120423 /* FSNotifier.swift */; }; + C41ADCE92970CCC700120423 /* FSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41ADCE72970CCC700120423 /* FSNotifier.swift */; }; + C41ADCEA2970CCC700120423 /* FSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41ADCE72970CCC700120423 /* FSNotifier.swift */; }; + C41ADCEB2970CCC700120423 /* FSNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41ADCE72970CCC700120423 /* FSNotifier.swift */; }; C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A827E61A65009F26CB /* FakeValetSite.swift */; }; C41C02AB27E61CB3009F26CB /* FakeValetSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C02A827E61A65009F26CB /* FakeValetSite.swift */; }; C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; }; @@ -92,6 +119,7 @@ C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; }; C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */; }; C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41E87192763D42300161EE0 /* DomainListVC+ContextMenu.swift */; }; + C41F3D08298AED0D0042ACBF /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D3660A29113F20006BD146 /* System.swift */; }; C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; }; C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4205A7D27F4D21800191A39 /* ValetProxy.swift */; }; C422DDAA28A2C49900CEAC97 /* WarningListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C422DDA928A2C49900CEAC97 /* WarningListView.swift */; }; @@ -109,9 +137,25 @@ C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */; }; C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; }; C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */; }; + C436B39D29F3C42500B6A64E /* PreferencesTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */; }; + C436B39E29F3C42500B6A64E /* PreferencesTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */; }; + C436B39F29F3C42500B6A64E /* PreferencesTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */; }; + C436B3A029F3C42500B6A64E /* PreferencesTabs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */; }; + C43931C529C4BD610069165B /* PhpFormulaeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C429C4BD610069165B /* PhpFormulaeView.swift */; }; + C43931C629C4BD610069165B /* PhpFormulaeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C429C4BD610069165B /* PhpFormulaeView.swift */; }; + C43931C729C4BD610069165B /* PhpFormulaeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C429C4BD610069165B /* PhpFormulaeView.swift */; }; + C43931C829C4BD610069165B /* PhpFormulaeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C429C4BD610069165B /* PhpFormulaeView.swift */; }; + C43931CA29C4C03F0069165B /* Brew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C929C4C03F0069165B /* Brew.swift */; }; + C43931CB29C4C03F0069165B /* Brew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C929C4C03F0069165B /* Brew.swift */; }; + C43931CC29C4C03F0069165B /* Brew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C929C4C03F0069165B /* Brew.swift */; }; + C43931CD29C4C03F0069165B /* Brew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43931C929C4C03F0069165B /* Brew.swift */; }; C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A1925D9CD1000591B77 /* Utility.swift */; }; C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */ = {isa = PBXBuildFile; fileRef = C43A8A1F25D9D1D700591B77 /* brew-formula.json */; }; C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */; }; + C43BCD4429FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; + C43BCD4529FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; + C43BCD4629FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; + C43BCD4729FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */; }; C43FDBE929A932B0003D85EC /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F427E2582B0045BD4E /* DomainListNameCell.swift */; }; C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44067F627E258410045BD4E /* DomainListPhpCell.swift */; }; @@ -168,6 +212,10 @@ C45B91542956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; C45B91552956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; C45B91562956123A00F4EC78 /* FakeServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45B91522956123A00F4EC78 /* FakeServicesManager.swift */; }; + C45D654C29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */; }; + C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */; }; + C45D654E29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */; }; + C45D654F29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */; }; C45E2A7529199248005C7CFD /* InternalSwitcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C471E7AF28F9B4940021E251 /* InternalSwitcherTest.swift */; }; C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E2A76291992DA005C7CFD /* FeatureTestCase.swift */; }; C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; @@ -240,19 +288,19 @@ C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415D3B62770F294005EF286 /* Actions.swift */; }; C471E7F028F9BAC30021E251 /* Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853B2770FE3900DA4FBE /* Paths.swift */; }; C471E7F128F9BAC70021E251 /* VersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */; }; - C471E7F228F9BAC70021E251 /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; + C471E7F228F9BAC70021E251 /* PhpEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */; }; C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; }; C471E7F428F9BAC80021E251 /* VersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */; }; - C471E7F528F9BAC80021E251 /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; + C471E7F528F9BAC80021E251 /* PhpEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */; }; C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; }; C471E7F728F9BACB0021E251 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; }; C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */; }; C471E7FA28F9BACB0021E251 /* InternalSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */; }; C471E7FB28F9BACE0021E251 /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; - C471E7FC28F9BACE0021E251 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; }; C471E7FD28F9BACE0021E251 /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; - C471E7FE28F9BACE0021E251 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C471E7FE28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; }; C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; }; C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; }; C471E80128F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; @@ -293,8 +341,8 @@ C471E82428F9BB2E0021E251 /* PhpFrameworks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */; }; C471E82528F9BB2E0021E251 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; }; C471E82628F9BB2E0021E251 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; - C471E82728F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; - C471E82828F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; + C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */; }; + C471E82828F9BB310021E251 /* BrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */; }; C471E82928F9BB330021E251 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C471E82A28F9BB330021E251 /* ValetListable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42F26722805B4B400938AC7 /* ValetListable.swift */; }; C471E82B28F9BB340021E251 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; @@ -325,8 +373,7 @@ C471E84828F9BB650021E251 /* EnvironmentCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */; }; C471E84A28F9BB650021E251 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; }; C471E84B28F9BB650021E251 /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; - C471E84C28F9BB650021E251 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */; }; - C471E84D28F9BB650021E251 /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; + C471E84D28F9BB650021E251 /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; C471E84E28F9BB650021E251 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; }; C471E84F28F9BB650021E251 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C471E85028F9BB650021E251 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; @@ -355,7 +402,7 @@ C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; }; C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; }; C471E86928F9BB650021E251 /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; }; - C471E86A28F9BB650021E251 /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; }; + C471E86A28F9BB650021E251 /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; }; C471E86B28F9BB650021E251 /* PreferenceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C450C8C528C919EC002A2B4B /* PreferenceName.swift */; }; C471E86C28F9BB650021E251 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; C471E86D28F9BB650021E251 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; @@ -414,8 +461,7 @@ C471E8AB28F9BB8F0021E251 /* EnvironmentCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */; }; C471E8AD28F9BB8F0021E251 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40FE736282ABA4F00A302C2 /* AppVersion.swift */; }; C471E8AE28F9BB8F0021E251 /* ServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C45E76132854A65300B4FE0C /* ServicesManager.swift */; }; - C471E8AF28F9BB8F0021E251 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */; }; - C471E8B028F9BB8F0021E251 /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; + C471E8B028F9BB8F0021E251 /* Valet+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */; }; C471E8B128F9BB8F0021E251 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; }; C471E8B228F9BB8F0021E251 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C471E8B328F9BB8F0021E251 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; @@ -444,7 +490,7 @@ C471E8CA28F9BB8F0021E251 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; }; C471E8CB28F9BB8F0021E251 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; }; C471E8CC28F9BB8F0021E251 /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; }; - C471E8CD28F9BB8F0021E251 /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; }; + C471E8CD28F9BB8F0021E251 /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; }; C471E8CE28F9BB8F0021E251 /* PreferenceName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C450C8C528C919EC002A2B4B /* PreferenceName.swift */; }; C471E8CF28F9BB8F0021E251 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; C471E8D028F9BB8F0021E251 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; @@ -489,7 +535,7 @@ C47DF1B2299D5A3B0007055D /* LoginItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47DF1AE299D5A3B0007055D /* LoginItemManager.swift */; }; C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; }; C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; }; - C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; }; + C481F79726164A78004FBCFF /* PreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PreferencesVC.swift */; }; C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; C485706D28BF450900539B36 /* NSMenuItemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40508B028ADAB44008FAC1F /* NSMenuItemExtension.swift */; }; C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; }; @@ -508,21 +554,35 @@ C485707B28BF458900539B36 /* VersionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44264BF2850BD2A007400F1 /* VersionPopoverView.swift */; }; C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C708C28AA7F7900E8D498 /* NoWarningsView.swift */; }; C485707D28BF45A200539B36 /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4297F7928970D59004C4630 /* WarningView.swift */; }; + C489E0BB2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */; }; + C489E0BC2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */; }; + C489E0BD2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */; }; + C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */; }; C48D0C9325CC804200CC7490 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; }; C48D6C70279CD2AC00F26D7E /* VersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */; }; C48D6C71279CD2AC00F26D7E /* VersionNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */; }; C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */; }; - C491997729901DD6001F3A21 /* CaskFileParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997629901DD6001F3A21 /* CaskFileParserTest.swift */; }; - C491997929901DE2001F3A21 /* phpmon-dev.rb in Resources */ = {isa = PBXBuildFile; fileRef = C491997829901DE2001F3A21 /* phpmon-dev.rb */; }; - C491997B29901DF7001F3A21 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997A29901DF7001F3A21 /* CaskFile.swift */; }; - C491997C29901DF7001F3A21 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997A29901DF7001F3A21 /* CaskFile.swift */; }; - C491997D29901DF7001F3A21 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997A29901DF7001F3A21 /* CaskFile.swift */; }; - C491997E29901DF7001F3A21 /* CaskFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997A29901DF7001F3A21 /* CaskFile.swift */; }; - C491998029901E0F001F3A21 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997F29901E0F001F3A21 /* AppUpdater.swift */; }; - C491998129901E0F001F3A21 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997F29901E0F001F3A21 /* AppUpdater.swift */; }; - C491998229901E0F001F3A21 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997F29901E0F001F3A21 /* AppUpdater.swift */; }; - C491998329901E0F001F3A21 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C491997F29901E0F001F3A21 /* AppUpdater.swift */; }; - C491998A29902089001F3A21 /* PHP Monitor Self-Updater.app in Resources */ = {isa = PBXBuildFile; fileRef = C491998929902089001F3A21 /* PHP Monitor Self-Updater.app */; }; + C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; + C48DDD0E29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; + C48DDD0F29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; + C48DDD1029C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */; }; + C490E3A729BC940D006D2DE6 /* ProgressWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */; }; + C490E3AA29BC9B3E006D2DE6 /* ProgressViewSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */; }; + C490E3B029BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */; }; + C490E3B129BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */; }; + C490E3B229BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */; }; + C490E3B329BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */; }; + C490E3B429BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */; }; + C490E3B529BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */; }; + C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; + C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; + C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; + C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */; }; + C490E3BB29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; + C490E3BC29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; + C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; + C490E3BE29BCA375006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; + C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49EAA5129B12A5A00AB28FC /* Measurements.swift */; }; C4927F0B27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.swift */; }; C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4927F0A27B2DFC200C55AFD /* Errors.swift */; }; C493084A279F331F009C240B /* AddSiteVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4930849279F331F009C240B /* AddSiteVC.swift */; }; @@ -531,12 +591,14 @@ C495F5B028A42E080087F70A /* EnvironmentCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */; }; C4998F0A2617633900B2526E /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; }; C4998F0B2617633900B2526E /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4998F092617633900B2526E /* PreferencesWindowController.swift */; }; - C4A6957628D23EE300A14CF8 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */; }; - C4A6957728D23EE300A14CF8 /* EnvironmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */; }; C4A81CA428C67101008DD9D1 /* PMTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A81CA328C67101008DD9D1 /* PMTableView.swift */; }; C4A81CA528C67101008DD9D1 /* PMTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4A81CA328C67101008DD9D1 /* PMTableView.swift */; }; C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */; }; C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; }; + C4ACE9E129F84EDD00110766 /* PhpGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACE9E029F84EDD00110766 /* PhpGuard.swift */; }; + C4ACE9E229F84EDD00110766 /* PhpGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACE9E029F84EDD00110766 /* PhpGuard.swift */; }; + C4ACE9E329F84EDD00110766 /* PhpGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACE9E029F84EDD00110766 /* PhpGuard.swift */; }; + C4ACE9E429F84EDD00110766 /* PhpGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACE9E029F84EDD00110766 /* PhpGuard.swift */; }; C4AD38B228ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AD38B128ECD9D300FA8D83 /* TestableFileSystem.swift */; }; C4AD38B328ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AD38B128ECD9D300FA8D83 /* TestableFileSystem.swift */; }; C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AF9F70275445FF00D44ED0 /* valet-config.json */; }; @@ -544,6 +606,12 @@ C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F792754499000D44ED0 /* Valet.swift */; }; C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */; }; + C4AFC4AE29C4F32F00BF4E0D /* BrewFormula.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */; }; + C4AFC4AF29C4F32F00BF4E0D /* BrewFormula.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */; }; + C4AFC4B029C4F32F00BF4E0D /* BrewFormula.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */; }; + C4AFC4B129C4F32F00BF4E0D /* BrewFormula.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */; }; + C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */; }; + C4AFC4B829C4F6DC00BF4E0D /* brew-outdated.json in Resources */ = {isa = PBXBuildFile; fileRef = C4AFC4B729C4F57B00BF4E0D /* brew-outdated.json */; }; 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 */; }; @@ -553,6 +621,22 @@ C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B5853D2770FE3900DA4FBE /* RealCommand.swift */; }; C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B609192853AAD300C95265 /* SectionHeaderView.swift */; }; C4B6091D2853AB9700C95265 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B6091C2853AB9700C95265 /* ServicesView.swift */; }; + C4B79EB629CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */; }; + C4B79EB729CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */; }; + C4B79EB829CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */; }; + C4B79EB929CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */; }; + C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */; }; + C4B79EBD29CA38DB00A483EE /* BrewCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */; }; + C4B79EBE29CA38DB00A483EE /* BrewCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */; }; + C4B79EBF29CA38DB00A483EE /* BrewCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */; }; + C4B79EC629CA474200A483EE /* FakeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EC529CA474200A483EE /* FakeCommand.swift */; }; + C4B79EC729CA474200A483EE /* FakeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EC529CA474200A483EE /* FakeCommand.swift */; }; + C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EC529CA474200A483EE /* FakeCommand.swift */; }; + C4B79EC929CA474200A483EE /* FakeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79EC529CA474200A483EE /* FakeCommand.swift */; }; + C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */; }; + C4B79ECC29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */; }; + C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */; }; + C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.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 */; }; @@ -580,6 +664,8 @@ C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3643828AE4FCE00C0770E /* StatusMenu+Items.swift */; }; C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4C3ED4327834C5200AB15D8 /* CustomPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */; }; + C4C75F5A298C2D5700DFD82E /* LaunchControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C75F59298C2D5700DFD82E /* LaunchControl.swift */; }; + C4C75F5C298C31C000DFD82E /* Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C75F5B298C31C000DFD82E /* Utility.swift */; }; C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */; }; C4C8900528F0E3D100CE5E97 /* RealFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */; }; C4C8900728F0E3EF00CE5E97 /* ActiveFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */; }; @@ -587,6 +673,7 @@ C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; + C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB250429B28BB800CA4492 /* MainMenuTest.swift */; }; C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CB6E67292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; @@ -620,6 +707,14 @@ C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D36619291173EA006BD146 /* DictionaryExtension.swift */; }; C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D36619291173EA006BD146 /* DictionaryExtension.swift */; }; C4D3661D291173EA006BD146 /* DictionaryExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D36619291173EA006BD146 /* DictionaryExtension.swift */; }; + C4D4CB3729C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */; }; + C4D4CB3829C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */; }; + C4D4CB3929C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */; }; + C4D4CB3A29C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */; }; + C4D5576429C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */; }; + C4D5576529C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */; }; + C4D5576629C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */; }; + C4D5576729C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */; }; C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */; }; C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; @@ -669,14 +764,15 @@ C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DEC28F764A00026AC4E /* TestableCommand.swift */; }; C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E49DEC28F764A00026AC4E /* TestableCommand.swift */; }; C4E9D2C02878B336008FFDAD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E9D2BF2878B336008FFDAD /* OnboardingView.swift */; }; + C4E9D90129CBA09E00BD28D4 /* PHP Monitor Self-Updater.app in Resources */ = {isa = PBXBuildFile; fileRef = C4E9D90029CBA09E00BD28D4 /* PHP Monitor Self-Updater.app */; }; C4EB53E528551F9B006F9937 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB53E428551F9B006F9937 /* HeaderView.swift */; }; C4EB53E728553117006F9937 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EB53E628553117006F9937 /* ArrayExtension.swift */; }; C4EC1E73279DFCF40010F296 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; }; C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EED88827A48778006D7272 /* InterAppHandler.swift */; }; - C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; - C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; + C4F2E4372752F0870020E974 /* BrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */; }; + C4F2E4382752F08D0020E974 /* BrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */; }; C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F30B02278E16BA00755FCE /* HomebrewService.swift */; }; @@ -701,7 +797,7 @@ C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0C9225CC804200CC7490 /* XibLoadable.swift */; }; C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; }; C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; - C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; + C4F780CA25D80B75000DBC97 /* HomebrewDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */; }; C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; }; C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; }; @@ -711,9 +807,9 @@ C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE82288F1F9700FC478F /* OnboardingWindowController.swift */; }; C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FBFC512616485F00CDB8E1 /* PhpVersionDetectionTest.swift */; }; C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F361602836BFD9003598CC /* MainMenu+Actions.swift */; }; - C4FD87A529AB98720002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; - C4FD87A629AB98730002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; - C4FD87A729AB98730002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; + C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; + C4FD87A929AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; + C4FD87AA29AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */; }; C4FE011128084FC200D1DE6D /* SelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FE011028084FC200D1DE6D /* SelectionVC.swift */; }; C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FE011028084FC200D1DE6D /* SelectionVC.swift */; }; /* End PBXBuildFile section */ @@ -744,7 +840,7 @@ /* Begin PBXFileReference section */ 03E36FE628D9219000636F7F /* ActiveShell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveShell.swift; sourceTree = ""; }; - 5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = ""; }; + 5420395826135DC100FB00FA /* PreferencesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesVC.swift; sourceTree = ""; }; 5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 5489625728312FAD004F647A /* CreatedFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatedFromFile.swift; sourceTree = ""; }; 54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy-custom-tld.test"; sourceTree = ""; }; @@ -768,16 +864,27 @@ C4068CA327B0780A00544CD5 /* CheckboxPreferenceView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CheckboxPreferenceView.xib; sourceTree = ""; }; C4068CA627B07A1300544CD5 /* SelectPreferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectPreferenceView.swift; sourceTree = ""; }; C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = ""; }; + C406A5F0298AD2CE00B5B85A /* PHP Monitor Self-Updater.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor Self-Updater.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + C406A5F2298AD2CE00B5B85A /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + C406A5F6298AD2CF00B5B85A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + C406A5FB298AD2CF00B5B85A /* phpmon-updater.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "phpmon-updater.entitlements"; sourceTree = ""; }; + C406A601298AD50D00B5B85A /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlert.swift; sourceTree = ""; }; C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlertVC.swift; sourceTree = ""; }; + C409349C298EE8E900D25014 /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; }; + C40934A1298EEB2C00D25014 /* CaskFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskFile.swift; sourceTree = ""; }; + C40934A6298EEB8700D25014 /* phpmon-dev.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = "phpmon-dev.rb"; sourceTree = ""; }; + C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaskFileParserTest.swift; sourceTree = ""; }; C40C5C9B2846A40600E28255 /* Preset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preset.swift; sourceTree = ""; }; - C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = ""; }; - C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = ""; }; + C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnvironments.swift; sourceTree = ""; }; + C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Valet+Alerts.swift"; sourceTree = ""; }; C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFormulaeStatus.swift; sourceTree = ""; }; + C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormulaUI.swift; sourceTree = ""; }; C40F505428ECA64E004AD45B /* TestableConfigurations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableConfigurations.swift; sourceTree = ""; }; C40FE736282ABA4F00A302C2 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; C40FE739282ABB2E00A302C2 /* AppVersionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionTest.swift; sourceTree = ""; }; - C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = ""; }; + C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDecodable.swift; sourceTree = ""; }; C413E43428DA3EB100AE33C7 /* TestableShellTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableShellTest.swift; sourceTree = ""; }; C415937E27A1B54F00D2E1B7 /* PhpFrameworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFrameworks.swift; sourceTree = ""; }; C4159AF628E4D40400545349 /* RealShellTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealShellTest.swift; sourceTree = ""; }; @@ -786,6 +893,7 @@ C4168F4427ADB4A3003B6C39 /* DEVELOPER.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPER.md; sourceTree = ""; }; C417DC73277614690015E6EE /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; C4181F1028FAF9330042EA28 /* UITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestCase.swift; sourceTree = ""; }; + C41ADCE72970CCC700120423 /* FSNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSNotifier.swift; sourceTree = ""; }; C41C02A827E61A65009F26CB /* FakeValetSite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeValetSite.swift; sourceTree = ""; }; C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -813,9 +921,13 @@ C42F26722805B4B400938AC7 /* ValetListable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetListable.swift; sourceTree = ""; }; C42F26752805FEE200938AC7 /* nginx-secure-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy.test"; sourceTree = ""; }; C436039F275E67610028EFC6 /* AppDelegate+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Notifications.swift"; sourceTree = ""; }; + C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesTabs.swift; sourceTree = ""; }; + C43931C429C4BD610069165B /* PhpFormulaeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpFormulaeView.swift; sourceTree = ""; }; + C43931C929C4C03F0069165B /* Brew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brew.swift; sourceTree = ""; }; C43A8A1925D9CD1000591B77 /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; C43A8A1F25D9D1D700591B77 /* brew-formula.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "brew-formula.json"; sourceTree = ""; }; C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackageTest.swift; sourceTree = ""; }; + C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAndUpgradeCommand.swift; sourceTree = ""; }; C43FDBE829A932B0003D85EC /* PhpConfigChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigChecker.swift; sourceTree = ""; }; C44067F427E2582B0045BD4E /* DomainListNameCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListNameCell.swift; sourceTree = ""; }; C44067F627E258410045BD4E /* DomainListPhpCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListPhpCell.swift; sourceTree = ""; }; @@ -842,6 +954,7 @@ C45B9148295607F400F4EC78 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = ""; }; C45B914D295608E300F4EC78 /* ValetServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetServicesManager.swift; sourceTree = ""; }; C45B91522956123A00F4EC78 /* FakeServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeServicesManager.swift; sourceTree = ""; }; + C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewPermissionFixer.swift; sourceTree = ""; }; C45E2A76291992DA005C7CFD /* FeatureTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureTestCase.swift; sourceTree = ""; }; C45E76132854A65300B4FE0C /* ServicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesManager.swift; sourceTree = ""; }; C463E37F284930EE00422731 /* PresetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetHelper.swift; sourceTree = ""; }; @@ -871,33 +984,41 @@ C47DF1AE299D5A3B0007055D /* LoginItemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginItemManager.swift; sourceTree = ""; }; C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; + C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeBrewFormulaeHandler.swift; sourceTree = ""; }; C48D0C9225CC804200CC7490 /* XibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XibLoadable.swift; sourceTree = ""; }; C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNumber.swift; sourceTree = ""; }; C48D6C73279CD3E400F26D7E /* PhpVersionNumberTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpVersionNumberTest.swift; sourceTree = ""; }; - C491997629901DD6001F3A21 /* CaskFileParserTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaskFileParserTest.swift; sourceTree = ""; }; - C491997829901DE2001F3A21 /* phpmon-dev.rb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.ruby; path = "phpmon-dev.rb"; sourceTree = ""; }; - C491997A29901DF7001F3A21 /* CaskFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaskFile.swift; sourceTree = ""; }; - C491997F29901E0F001F3A21 /* AppUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; }; - C491998929902089001F3A21 /* PHP Monitor Self-Updater.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; path = "PHP Monitor Self-Updater.app"; sourceTree = ""; }; + C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockingOverlayView.swift; sourceTree = ""; }; + C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressWindowView.swift; sourceTree = ""; }; + C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressViewSubject.swift; sourceTree = ""; }; C4927F0A27B2DFC200C55AFD /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; C4930849279F331F009C240B /* AddSiteVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteVC.swift; sourceTree = ""; }; C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentCheck.swift; sourceTree = ""; }; C4998F092617633900B2526E /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; - C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentManager.swift; sourceTree = ""; }; + C49EAA5129B12A5A00AB28FC /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = ""; }; + C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+BrewWatch.swift"; sourceTree = ""; }; C4A81CA328C67101008DD9D1 /* PMTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMTableView.swift; sourceTree = ""; }; C4AC51FB27E27F47008528CA /* DomainListKindCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListKindCell.swift; sourceTree = ""; }; C4ACA38E25C754C100060C66 /* PhpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtension.swift; sourceTree = ""; }; + C4ACE9E029F84EDD00110766 /* PhpGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpGuard.swift; sourceTree = ""; }; C4AD38B128ECD9D300FA8D83 /* TestableFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableFileSystem.swift; sourceTree = ""; }; C4AF9F70275445FF00D44ED0 /* valet-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "valet-config.json"; sourceTree = ""; }; C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetConfigurationTest.swift; sourceTree = ""; }; C4AF9F792754499000D44ED0 /* Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Valet.swift; sourceTree = ""; }; C4AF9F7C275454A900D44ED0 /* ValetVersionExtractorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValetVersionExtractorTest.swift; sourceTree = ""; }; + C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormula.swift; sourceTree = ""; }; + C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewUpgradableTest.swift; sourceTree = ""; }; + C4AFC4B729C4F57B00BF4E0D /* brew-outdated.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "brew-outdated.json"; 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 = ""; }; C4B5853B2770FE3900DA4FBE /* Paths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Paths.swift; sourceTree = ""; }; C4B5853D2770FE3900DA4FBE /* RealCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealCommand.swift; sourceTree = ""; }; C4B609192853AAD300C95265 /* SectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = ""; }; C4B6091C2853AB9700C95265 /* ServicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesView.swift; sourceTree = ""; }; + C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewFormulaeHandler.swift; sourceTree = ""; }; + C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewCommand.swift; sourceTree = ""; }; + C4B79EC529CA474200A483EE /* FakeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeCommand.swift; sourceTree = ""; }; + C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovePhpVersionCommand.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 = ""; }; @@ -910,11 +1031,14 @@ C4C3643828AE4FCE00C0770E /* StatusMenu+Items.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Items.swift"; sourceTree = ""; }; C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Startup.swift"; sourceTree = ""; }; C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPrefs.swift; sourceTree = ""; }; + C4C75F59298C2D5700DFD82E /* LaunchControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchControl.swift; sourceTree = ""; }; + C4C75F5B298C31C000DFD82E /* Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utility.swift; sourceTree = ""; }; C4C8900228F0E28800CE5E97 /* FileSystemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemProtocol.swift; sourceTree = ""; }; C4C8900428F0E3D100CE5E97 /* RealFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealFileSystem.swift; sourceTree = ""; }; C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveFileSystem.swift; sourceTree = ""; }; C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; }; C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = ""; }; + C4CB250429B28BB800CA4492 /* MainMenuTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuTest.swift; sourceTree = ""; }; C4CB6E64292C362C002E9027 /* Homebrew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Homebrew.swift; sourceTree = ""; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; C4CDA892288F1A71007CE25F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = ""; }; @@ -926,6 +1050,8 @@ C4D3660F291140BE006BD146 /* TestableFileSystemTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestableFileSystemTest.swift; sourceTree = ""; }; C4D36614291160A1006BD146 /* WIP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIP.swift; sourceTree = ""; }; C4D36619291173EA006BD146 /* DictionaryExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryExtension.swift; sourceTree = ""; }; + C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InternalSwitcher+Valet.swift"; sourceTree = ""; }; + C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionManagerWC.swift; sourceTree = ""; }; C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationFile.swift; sourceTree = ""; }; C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; }; C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = ""; }; @@ -948,12 +1074,13 @@ C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = ""; }; C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = ""; }; C4E9D2BF2878B336008FFDAD /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + C4E9D90029CBA09E00BD28D4 /* PHP Monitor Self-Updater.app */ = {isa = PBXFileReference; lastKnownFileType = wrapper.application; path = "PHP Monitor Self-Updater.app"; sourceTree = ""; }; C4EB53E428551F9B006F9937 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; C4EB53E628553117006F9937 /* ArrayExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtension.swift; sourceTree = ""; }; C4EC1E72279DFCF40010F296 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = ""; }; C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C4EED88827A48778006D7272 /* InterAppHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterAppHandler.swift; sourceTree = ""; }; - C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = ""; }; + C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrewDiagnostics.swift; sourceTree = ""; }; C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = ""; }; C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = ""; }; C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = ""; }; @@ -972,6 +1099,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + C406A5ED298AD2CE00B5B85A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C41C1B3022B0097F00E7CF16 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1008,7 +1142,8 @@ children = ( C4998F092617633900B2526E /* PreferencesWindowController.swift */, C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */, - 5420395826135DC100FB00FA /* PrefsVC.swift */, + 5420395826135DC100FB00FA /* PreferencesVC.swift */, + C436B39C29F3C42500B6A64E /* PreferencesTabs.swift */, C450C8C528C919EC002A2B4B /* PreferenceName.swift */, 5420395E2613607600FB00FA /* Preferences.swift */, C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */, @@ -1097,6 +1232,20 @@ path = IAP; sourceTree = ""; }; + C406A5F1298AD2CE00B5B85A /* phpmon-updater */ = { + isa = PBXGroup; + children = ( + C4E9D90029CBA09E00BD28D4 /* PHP Monitor Self-Updater.app */, + C406A5F2298AD2CE00B5B85A /* main.swift */, + C406A601298AD50D00B5B85A /* Updater.swift */, + C4C75F5B298C31C000DFD82E /* Utility.swift */, + C4C75F59298C2D5700DFD82E /* LaunchControl.swift */, + C406A5F6298AD2CF00B5B85A /* Assets.xcassets */, + C406A5FB298AD2CF00B5B85A /* phpmon-updater.entitlements */, + ); + path = "phpmon-updater"; + sourceTree = ""; + }; C4080FF827BD955900BF2C6B /* Notice */ = { isa = PBXGroup; children = ( @@ -1161,7 +1310,7 @@ C4F5FBCC28218C93001065C5 /* .swiftlint.yml */, C4E713572570151400007428 /* docs */, C41C1B3522B0097F00E7CF16 /* phpmon */, - C491998829902061001F3A21 /* phpmon-updater */, + C406A5F1298AD2CE00B5B85A /* phpmon-updater */, C471E79628F9B4260021E251 /* tests */, C41C1B3422B0097F00E7CF16 /* Products */, C4D309E72770EF2F00958BCF /* Frameworks */, @@ -1175,6 +1324,7 @@ C4F7807925D7F84B000DBC97 /* Unit Tests.xctest */, C471E7AD28F9B4940021E251 /* Feature Tests.xctest */, C471E7BC28F9B90F0021E251 /* UI Tests.xctest */, + C406A5F0298AD2CE00B5B85A /* PHP Monitor Self-Updater.app */, ); name = Products; sourceTree = ""; @@ -1252,6 +1402,19 @@ path = Warning; sourceTree = ""; }; + C43931C329C4BD510069165B /* PhpManager */ = { + isa = PBXGroup; + children = ( + C4D5576329C77CC5001A44CD /* PhpVersionManagerWC.swift */, + C43931C429C4BD610069165B /* PhpFormulaeView.swift */, + C48DDD0C29C75C9E00D032D9 /* BlockingOverlayView.swift */, + C40D72592A018ACC0054A067 /* PhpFormulaeStatus.swift */, + C40D725E2A018AE30054A067 /* BrewFormulaUI.swift */, + C489E0BA2A220A4200323F5E /* FakeBrewFormulaeHandler.swift */, + ); + path = PhpManager; + sourceTree = ""; + }; C43FDBE729A9329A003D85EC /* Services */ = { isa = PBXGroup; children = ( @@ -1331,7 +1494,8 @@ C459B4BF27F6094100E9B4B4 /* brew */ = { isa = PBXGroup; children = ( - C491997829901DE2001F3A21 /* phpmon-dev.rb */, + C40934A6298EEB8700D25014 /* phpmon-dev.rb */, + C4AFC4B729C4F57B00BF4E0D /* brew-outdated.json */, C4E2E85228FC256B003B070C /* brew-services-normal.json */, C4E2E85128FC256B003B070C /* brew-services-sudo.json */, C43A8A1F25D9D1D700591B77 /* brew-formula.json */, @@ -1358,6 +1522,14 @@ path = php; sourceTree = ""; }; + C45B42C329C7C67400366A14 /* Fake */ = { + isa = PBXGroup; + children = ( + C4B79EC529CA474200A483EE /* FakeCommand.swift */, + ); + path = Fake; + sourceTree = ""; + }; C45B9147295607E200F4EC78 /* Services */ = { isa = PBXGroup; children = ( @@ -1369,6 +1541,14 @@ path = Services; sourceTree = ""; }; + C45D654A29F52F5F004C28F9 /* Behaviors */ = { + isa = PBXGroup; + children = ( + C45D654B29F52F74004C28F9 /* BrewPermissionFixer.swift */, + ); + path = Behaviors; + sourceTree = ""; + }; C464ADAA275A7A25003FCD53 /* DomainList */ = { isa = PBXGroup; children = ( @@ -1432,6 +1612,7 @@ C469E702294CFDF700A82AB2 /* DomainsListTest.swift */, C44E985E29B23EBF0059F773 /* UpdateCheckTest.swift */, C4181F1028FAF9330042EA28 /* UITestCase.swift */, + C4CB250429B28BB800CA4492 /* MainMenuTest.swift */, ); path = ui; sourceTree = ""; @@ -1462,7 +1643,9 @@ C4B5635D276AB09000F12CCB /* VersionExtractor.swift */, C4D3660A29113F20006BD146 /* System.swift */, C4D36614291160A1006BD146 /* WIP.swift */, + C41ADCE72970CCC700120423 /* FSNotifier.swift */, C47DF1AE299D5A3B0007055D /* LoginItemManager.swift */, + C49EAA5129B12A5A00AB28FC /* Measurements.swift */, ); path = Helpers; sourceTree = ""; @@ -1472,24 +1655,26 @@ children = ( C48D6C6F279CD2AC00F26D7E /* VersionNumber.swift */, C4CE7F9529683B43000102CF /* PhpVersionNumberCollection.swift */, - C40C7F1D2772136000DDDCDC /* PhpEnv.swift */, + C40C7F1D2772136000DDDCDC /* PhpEnvironments.swift */, C4D936C827E3EB6100BD69FE /* PhpHelper.swift */, ); path = "PHP Version"; sourceTree = ""; }; - C491998829902061001F3A21 /* phpmon-updater */ = { + C490E3A329BC92E6006D2DE6 /* Progress */ = { isa = PBXGroup; children = ( - C491998929902089001F3A21 /* PHP Monitor Self-Updater.app */, + C490E3A629BC940D006D2DE6 /* ProgressWindowView.swift */, + C490E3A929BC9B3E006D2DE6 /* ProgressViewSubject.swift */, ); - path = "phpmon-updater"; + path = Progress; sourceTree = ""; }; C4AF9F6A275445C900D44ED0 /* Valet */ = { isa = PBXGroup; children = ( C4AF9F792754499000D44ED0 /* Valet.swift */, + C40C7F2727721FF600DDDCDC /* Valet+Alerts.swift */, C40175B629030F7A00763A68 /* Domains */, C4EF72C9294BC6E60088B538 /* Scanners */, C4C0E8D927F887BD002D32A9 /* Proxies */, @@ -1513,8 +1698,14 @@ C4AF9F6C275445D900D44ED0 /* Homebrew */ = { isa = PBXGroup; children = ( - C491997A29901DF7001F3A21 /* CaskFile.swift */, - C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */, + C45D654A29F52F5F004C28F9 /* Behaviors */, + C4B79EBA29CA38D100A483EE /* Commands */, + C45B42C329C7C67400366A14 /* Fake */, + C43931C929C4C03F0069165B /* Brew.swift */, + C4AFC4AD29C4F32F00BF4E0D /* BrewFormula.swift */, + C4B79EB529CA387F00A483EE /* BrewFormulaeHandler.swift */, + C4F2E4362752F0870020E974 /* BrewDiagnostics.swift */, + C40934A1298EEB2C00D25014 /* CaskFile.swift */, ); path = Homebrew; sourceTree = ""; @@ -1535,8 +1726,7 @@ C4D8016522B1584700C6DA1B /* Startup.swift */, C495F5AE28A42E080087F70A /* EnvironmentCheck.swift */, C40FE736282ABA4F00A302C2 /* AppVersion.swift */, - C491997F29901E0F001F3A21 /* AppUpdater.swift */, - C4A6957528D23EE300A14CF8 /* EnvironmentManager.swift */, + C409349C298EE8E900D25014 /* AppUpdater.swift */, ); path = App; sourceTree = ""; @@ -1587,6 +1777,16 @@ path = Domains; sourceTree = ""; }; + C4B79EBA29CA38D100A483EE /* Commands */ = { + isa = PBXGroup; + children = ( + C4B79EBB29CA38DB00A483EE /* BrewCommand.swift */, + C43BCD4329FBEF40001547BC /* InstallAndUpgradeCommand.swift */, + C4B79ECA29CA475900A483EE /* RemovePhpVersionCommand.swift */, + ); + path = Commands; + sourceTree = ""; + }; C4C0E8D827F887A5002D32A9 /* Sites */ = { isa = PBXGroup; children = ( @@ -1623,7 +1823,8 @@ C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, C4551656297AED18009B8466 /* ValetRcTest.swift */, - C491997629901DD6001F3A21 /* CaskFileParserTest.swift */, + C40934AA298EEDA900D25014 /* CaskFileParserTest.swift */, + C4AFC4B229C4F43300BF4E0D /* HomebrewUpgradableTest.swift */, ); path = Parsers; sourceTree = ""; @@ -1661,6 +1862,7 @@ C4C8E81D276F5686003AC782 /* Watcher */ = { isa = PBXGroup; children = ( + C49EAA5629B1689200AB28FC /* App+BrewWatch.swift */, C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */, C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */, ); @@ -1687,7 +1889,7 @@ C4D9ADBD27761084007277F4 /* PHP */ = { isa = PBXGroup; children = ( - C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */, + C4ACE9E029F84EDD00110766 /* PhpGuard.swift */, ); path = PHP; sourceTree = ""; @@ -1697,6 +1899,7 @@ children = ( C4D9ADBE277610E1007277F4 /* PhpSwitcher.swift */, C4D9ADC7277611A0007277F4 /* InternalSwitcher.swift */, + C4D4CB3629C109CF00DB9F93 /* InternalSwitcher+Valet.swift */, ); path = Switcher; sourceTree = ""; @@ -1732,6 +1935,8 @@ C4EE55B027708BB2001DF387 /* SwiftUI */ = { isa = PBXGroup; children = ( + C43931C329C4BD510069165B /* PhpManager */, + C490E3A329BC92E6006D2DE6 /* Progress */, C4297F7828970D4E004C4630 /* Warning */, C4E9D2BE2878B32D008FFDAD /* Onboarding */, C4B609182853AAA700C95265 /* Domains */, @@ -1755,7 +1960,7 @@ C4F30B01278E169B00755FCE /* Homebrew */ = { isa = PBXGroup; children = ( - C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */, + C412E5FB25700D5300A1FB67 /* HomebrewDecodable.swift */, C4F30B02278E16BA00755FCE /* HomebrewService.swift */, ); path = Homebrew; @@ -1814,6 +2019,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + C406A5EF298AD2CE00B5B85A /* PHP Monitor Self-Updater */ = { + isa = PBXNativeTarget; + buildConfigurationList = C406A5FC298AD2CF00B5B85A /* Build configuration list for PBXNativeTarget "PHP Monitor Self-Updater" */; + buildPhases = ( + C406A5EC298AD2CE00B5B85A /* Sources */, + C406A5ED298AD2CE00B5B85A /* Frameworks */, + C406A5EE298AD2CE00B5B85A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "PHP Monitor Self-Updater"; + productName = "PHP Monitor Updater"; + productReference = C406A5F0298AD2CE00B5B85A /* PHP Monitor Self-Updater.app */; + productType = "com.apple.product-type.application"; + }; C41C1B3222B0097F00E7CF16 /* PHP Monitor */ = { isa = PBXNativeTarget; buildConfigurationList = C41C1B4322B0098000E7CF16 /* Build configuration list for PBXNativeTarget "PHP Monitor" */; @@ -1896,10 +2118,14 @@ C41C1B2B22B0097F00E7CF16 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1400; - LastUpgradeCheck = 1400; + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "Nico Verbruggen"; TargetAttributes = { + C406A5EF298AD2CE00B5B85A = { + CreatedOnToolsVersion = 14.2; + }; C41C1B3222B0097F00E7CF16 = { CreatedOnToolsVersion = 10.2.1; }; @@ -1931,6 +2157,7 @@ projectRoot = ""; targets = ( C41C1B3222B0097F00E7CF16 /* PHP Monitor */, + C406A5EF298AD2CE00B5B85A /* PHP Monitor Self-Updater */, C4F7807825D7F84B000DBC97 /* Unit Tests */, C471E7AC28F9B4940021E251 /* Feature Tests */, C471E7BB28F9B90F0021E251 /* UI Tests */, @@ -1939,14 +2166,22 @@ /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + C406A5EE298AD2CE00B5B85A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C406A5F7298AD2CF00B5B85A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C41C1B3122B0097F00E7CF16 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C4E9D90129CBA09E00BD28D4 /* PHP Monitor Self-Updater.app in Resources */, C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */, C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */, C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */, - C491998A29902089001F3A21 /* PHP Monitor Self-Updater.app in Resources */, C44C1991276E44CB0072762D /* ProgressWindow.storyboard in Resources */, C4232EE52612526500158FC6 /* Credits.html in Resources */, 54FCFD26276C883F004CE748 /* SelectPreferenceView.xib in Resources */, @@ -1964,6 +2199,7 @@ C4570C3B28FC355300D18420 /* Localizable.strings in Resources */, C4E2E85528FC256B003B070C /* brew-services-sudo.json in Resources */, C4E2E85928FC256B003B070C /* brew-services-normal.json in Resources */, + C40934A8298EEB8700D25014 /* phpmon-dev.rb in Resources */, C4E2E84F28FC22E4003B070C /* brew-formula.json in Resources */, C4E2E86228FC28A6003B070C /* brew-services.json in Resources */, ); @@ -1976,6 +2212,7 @@ C4570C3A28FC355300D18420 /* Localizable.strings in Resources */, C4E2E85628FC256B003B070C /* brew-services-sudo.json in Resources */, C4E2E85A28FC256B003B070C /* brew-services-normal.json in Resources */, + C40934A9298EEB8700D25014 /* phpmon-dev.rb in Resources */, C4E2E85028FC22E4003B070C /* brew-formula.json in Resources */, C4E2E86128FC28A6003B070C /* brew-services.json in Resources */, ); @@ -1998,11 +2235,12 @@ C4E2E85828FC256B003B070C /* brew-services-normal.json in Resources */, C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */, C42F26762805FEE200938AC7 /* nginx-secure-proxy.test in Resources */, + C4AFC4B829C4F6DC00BF4E0D /* brew-outdated.json in Resources */, C4F30B08278E195800755FCE /* brew-services.json in Resources */, C455165B297AEDB5009B8466 /* valetrc.broken in Resources */, 54A18D40282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test in Resources */, C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */, - C491997929901DE2001F3A21 /* phpmon-dev.rb in Resources */, + C40934A7298EEB8700D25014 /* phpmon-dev.rb in Resources */, C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */, C4E2E85428FC256B003B070C /* brew-services-sudo.json in Resources */, ); @@ -2033,16 +2271,36 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + C406A5EC298AD2CE00B5B85A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C4C75F5C298C31C000DFD82E /* Utility.swift in Sources */, + C490E3BC29BCA375006D2DE6 /* Measurements.swift in Sources */, + C406A602298AD50D00B5B85A /* Updater.swift in Sources */, + C4C75F5A298C2D5700DFD82E /* LaunchControl.swift in Sources */, + C41F3D08298AED0D0042ACBF /* System.swift in Sources */, + C406A5F3298AD2CE00B5B85A /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C41C1B2F22B0097F00E7CF16 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C489E0BB2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, + C41ADCE82970CCC700120423 /* FSNotifier.swift in Sources */, C47699EF28A2F2A30060FEB8 /* WarningManager.swift in Sources */, + C490E3BB29BCA375006D2DE6 /* Measurements.swift in Sources */, + C4B79EC629CA474200A483EE /* FakeCommand.swift in Sources */, C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */, C47DF1AF299D5A3B0007055D /* LoginItemManager.swift in Sources */, + C490E3AA29BC9B3E006D2DE6 /* ProgressViewSubject.swift in Sources */, C4D3661A291173EA006BD146 /* DictionaryExtension.swift in Sources */, C4C8900728F0E3EF00CE5E97 /* ActiveFileSystem.swift in Sources */, + C409349D298EE8E900D25014 /* AppUpdater.swift in Sources */, C4D8016622B1584700C6DA1B /* Startup.swift in Sources */, + C43931CA29C4C03F0069165B /* Brew.swift in Sources */, C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */, C48D6C70279CD2AC00F26D7E /* VersionNumber.swift in Sources */, C4998F0A2617633900B2526E /* PreferencesWindowController.swift in Sources */, @@ -2050,12 +2308,13 @@ C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, C4EB53E728553117006F9937 /* ArrayExtension.swift in Sources */, - 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, + 5420395926135DC100FB00FA /* PreferencesVC.swift in Sources */, C4C8900328F0E28800CE5E97 /* FileSystemProtocol.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C45B9149295607F400F4EC78 /* Service.swift in Sources */, 5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */, C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, + C48DDD0D29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, C45B91532956123A00F4EC78 /* FakeServicesManager.swift in Sources */, C41C708D28AA7F7900E8D498 /* NoWarningsView.swift in Sources */, C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */, @@ -2069,10 +2328,10 @@ C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C4D9F24B280B69E100DCD39A /* AddProxyVC.swift in Sources */, C45B914E295608E300F4EC78 /* ValetServicesManager.swift in Sources */, + C4D5576429C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */, C4E49DED28F764A00026AC4E /* TestableCommand.swift in Sources */, - C4A6957628D23EE300A14CF8 /* EnvironmentManager.swift in Sources */, C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, - C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, + C40C7F2827721FF600DDDCDC /* Valet+Alerts.swift in Sources */, C463E380284930EE00422731 /* PresetHelper.swift in Sources */, C41C02A927E61A65009F26CB /* FakeValetSite.swift in Sources */, C4E2E85C28FC282B003B070C /* TestableConfiguration.swift in Sources */, @@ -2081,7 +2340,7 @@ C4463FCC29804BCB007B93D5 /* RCFile.swift in Sources */, C44264BE2850B86C007400F1 /* SwiftUIHelper.swift in Sources */, C4E9D2C02878B336008FFDAD /* OnboardingView.swift in Sources */, - C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */, + C4F2E4372752F0870020E974 /* BrewDiagnostics.swift in Sources */, C4AD38B228ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */, C4EB53E528551F9B006F9937 /* HeaderView.swift in Sources */, C40FE737282ABA4F00A302C2 /* AppVersion.swift in Sources */, @@ -2090,8 +2349,10 @@ C4B585442770FE3900DA4FBE /* RealCommand.swift in Sources */, C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */, C40C5C9C2846A40600E28255 /* Preset.swift in Sources */, + C4B79EBC29CA38DB00A483EE /* BrewCommand.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C4B6091A2853AAD300C95265 /* SectionHeaderView.swift in Sources */, + C436B39D29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */, C4FACE80288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */, C415D3B72770F294005EF286 /* Actions.swift in Sources */, @@ -2099,7 +2360,9 @@ C4C3643928AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */, C4CDA893288F1A71007CE25F /* Keys.swift in Sources */, + C43931C529C4BD610069165B /* PhpFormulaeView.swift in Sources */, C40175B82903108900763A68 /* ValetInteractor.swift in Sources */, + C4ACE9E129F84EDD00110766 /* PhpGuard.swift in Sources */, C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */, C46EBC4A28DB966A007ACC74 /* TestableShell.swift in Sources */, C44C198D276E3A1C0072762D /* TerminalProgressWindowController.swift in Sources */, @@ -2111,12 +2374,12 @@ C4CE3BB827B31F2E0086CA49 /* MainMenu+Switcher.swift in Sources */, C415937F27A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */, C4811D2422D70A4700B5F6B3 /* App.swift in Sources */, + C40934A2298EEB2C00D25014 /* CaskFile.swift in Sources */, C495F5AF28A42E080087F70A /* EnvironmentCheck.swift in Sources */, C46EBC4428DB95F0007ACC74 /* ShellProtocol.swift in Sources */, C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */, C4F30B03278E16BA00755FCE /* HomebrewService.swift in Sources */, 54D9E0B427E4F51E003B9AD9 /* Key.swift in Sources */, - C491997B29901DF7001F3A21 /* CaskFile.swift in Sources */, C4297F7A28970D59004C4630 /* WarningView.swift in Sources */, C4C0E8E227F88B13002D32A9 /* ValetDomainScanner.swift in Sources */, C42F26732805B4B400938AC7 /* ValetListable.swift in Sources */, @@ -2128,10 +2391,11 @@ C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */, C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, - C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, + C412E5FC25700D5300A1FB67 /* HomebrewDecodable.swift in Sources */, 03E36FE728D9219000636F7F /* ActiveShell.swift in Sources */, C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */, C45E76142854A65300B4FE0C /* ServicesManager.swift in Sources */, + C4D4CB3729C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */, C46EBC4728DB9644007ACC74 /* RealShell.swift in Sources */, C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, C44264C02850BD2A007400F1 /* VersionPopoverView.swift in Sources */, @@ -2142,6 +2406,7 @@ C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, C422DDAA28A2C49900CEAC97 /* WarningListView.swift in Sources */, C469E6FE294CF7B200A82AB2 /* FakeValetProxy.swift in Sources */, + C490E3B629BCA367006D2DE6 /* App+BrewWatch.swift in Sources */, C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */, C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */, C4C1019B27C65C6F001FACC2 /* Process.swift in Sources */, @@ -2159,31 +2424,39 @@ C4B6091D2853AB9700C95265 /* ServicesView.swift in Sources */, C40508B128ADAB44008FAC1F /* NSMenuItemExtension.swift in Sources */, C4B97B7B275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */, + C45D654C29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4D3660B29113F20006BD146 /* System.swift in Sources */, C4D36601291132B7006BD146 /* ValetScanners.swift in Sources */, C4EED88927A48778006D7272 /* InterAppHandler.swift in Sources */, - C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */, + C40C7F1E2772136000DDDCDC /* PhpEnvironments.swift in Sources */, + C4B79EB629CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */, C476FF9822B0DD830098105B /* Alert.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C4D36615291160A1006BD146 /* WIP.swift in Sources */, C485707028BF452300539B36 /* WarningsWindowController.swift in Sources */, C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */, + C40D725A2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */, C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4FACE83288F1F9700FC478F /* OnboardingWindowController.swift in Sources */, C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */, C43FDBE929A932B0003D85EC /* PhpConfigChecker.swift in Sources */, + C490E3A729BC940D006D2DE6 /* ProgressWindowView.swift in Sources */, C4BF56AB2949381100379603 /* FakeValetInteractor.swift in Sources */, C4B5635E276AB09000F12CCB /* VersionExtractor.swift in Sources */, C451AFF62969E40F0078E617 /* HelpButton.swift in Sources */, 54D9E0B627E4F51E003B9AD9 /* HotKey.swift in Sources */, + C4AFC4AE29C4F32F00BF4E0D /* BrewFormula.swift in Sources */, C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */, C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */, C44067F927E2585E0045BD4E /* DomainListTypeCell.swift in Sources */, 54D9E0BA27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */, C4C3ED412783497000AB15D8 /* MainMenu+Startup.swift in Sources */, C40508AF28ADA23D008FAC1F /* NoDomainResultsView.swift in Sources */, + C4B79ECB29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, + C40D725F2A018AE30054A067 /* BrewFormulaUI.swift in Sources */, C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */, + C43BCD4429FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, C4E2E84A28FC1E70003B070C /* DataExtension.swift in Sources */, C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */, C42337A3281F19F000459A48 /* Xdebug.swift in Sources */, @@ -2194,7 +2467,6 @@ C4EE188422D3386B00E126E5 /* Constants.swift in Sources */, C493084A279F331F009C240B /* AddSiteVC.swift in Sources */, C4DEB7D427A5D60B00834718 /* Stats.swift in Sources */, - C491998029901E0F001F3A21 /* AppUpdater.swift in Sources */, C4E49DEA28F7643D0026AC4E /* CommandProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2206,14 +2478,17 @@ C471E82D28F9BB650021E251 /* AlertableError.swift in Sources */, C471E82E28F9BB650021E251 /* Errors.swift in Sources */, C471E82F28F9BB650021E251 /* Alert.swift in Sources */, + C4FD87A929AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */, C471E83028F9BB650021E251 /* Application.swift in Sources */, C471E83128F9BB650021E251 /* LocalNotification.swift in Sources */, + C4B79EB829CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */, C471E83228F9BB650021E251 /* MenuBarImageGenerator.swift in Sources */, C4BB393B2981AFC700F8E797 /* PhpVersionSource.swift in Sources */, C471E83328F9BB650021E251 /* PMWindowController.swift in Sources */, C471E83428F9BB650021E251 /* VersionExtractor.swift in Sources */, C471E83528F9BB650021E251 /* ValetProxy.swift in Sources */, C471E83728F9BB650021E251 /* DomainScanner.swift in Sources */, + C490E3BE29BCA375006D2DE6 /* Measurements.swift in Sources */, C471E83928F9BB650021E251 /* ValetSite.swift in Sources */, C471E83A28F9BB650021E251 /* FakeValetSite.swift in Sources */, C471E83C28F9BB650021E251 /* ValetDomainScanner.swift in Sources */, @@ -2226,8 +2501,6 @@ C471E84228F9BB650021E251 /* AppDelegate+InterApp.swift in Sources */, C471E84328F9BB650021E251 /* App.swift in Sources */, C4E2E85E28FC282B003B070C /* TestableConfiguration.swift in Sources */, - C491997D29901DF7001F3A21 /* CaskFile.swift in Sources */, - C4FD87A629AB98730002D701 /* PhpConfigChecker.swift in Sources */, C45E2A7529199248005C7CFD /* InternalSwitcherTest.swift in Sources */, C471E84428F9BB650021E251 /* App+ActivationPolicy.swift in Sources */, C471E84528F9BB650021E251 /* App+GlobalHotkey.swift in Sources */, @@ -2236,9 +2509,10 @@ C471E84828F9BB650021E251 /* EnvironmentCheck.swift in Sources */, C471E84A28F9BB650021E251 /* AppVersion.swift in Sources */, C471E84B28F9BB650021E251 /* ServicesManager.swift in Sources */, - C471E84C28F9BB650021E251 /* EnvironmentManager.swift in Sources */, - C471E84D28F9BB650021E251 /* ActivePhpInstallation+Checks.swift in Sources */, + C4D4CB3929C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */, + C471E84D28F9BB650021E251 /* Valet+Alerts.swift in Sources */, C471E84E28F9BB650021E251 /* MainMenu.swift in Sources */, + C40934A4298EEB2C00D25014 /* CaskFile.swift in Sources */, C471E84F28F9BB650021E251 /* MainMenu+Startup.swift in Sources */, C471E85028F9BB650021E251 /* MainMenu+Async.swift in Sources */, C471E85128F9BB650021E251 /* MainMenu+Switcher.swift in Sources */, @@ -2250,6 +2524,7 @@ C4D36617291160A1006BD146 /* WIP.swift in Sources */, C471E85728F9BB650021E251 /* DomainListTLSCell.swift in Sources */, C471E85828F9BB650021E251 /* DomainListNameCell.swift in Sources */, + C4B79EBE29CA38DB00A483EE /* BrewCommand.swift in Sources */, C47DF1B1299D5A3B0007055D /* LoginItemManager.swift in Sources */, C471E85928F9BB650021E251 /* DomainListPhpCell.swift in Sources */, C471E85A28F9BB650021E251 /* DomainListTypeCell.swift in Sources */, @@ -2260,12 +2535,16 @@ C471E85E28F9BB650021E251 /* DomainListVC+ContextMenu.swift in Sources */, C4E2E86628FC2F1B003B070C /* XCPMApplication.swift in Sources */, C471E85F28F9BB650021E251 /* DomainListVC+Actions.swift in Sources */, + C490E3B429BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */, + C4D5576629C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */, + C4ACE9E329F84EDD00110766 /* PhpGuard.swift in Sources */, C471E86028F9BB650021E251 /* SelectionVC.swift in Sources */, C471E86128F9BB650021E251 /* AddSiteVC.swift in Sources */, C471E86228F9BB650021E251 /* AddProxyVC.swift in Sources */, C471E86328F9BB650021E251 /* PMTableView.swift in Sources */, C471E86428F9BB650021E251 /* Warning.swift in Sources */, C40175BA2903108900763A68 /* ValetInteractor.swift in Sources */, + C43931C729C4BD610069165B /* PhpFormulaeView.swift in Sources */, C4463FCE29804BCB007B93D5 /* RCFile.swift in Sources */, C45B9150295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E86528F9BB650021E251 /* WarningManager.swift in Sources */, @@ -2273,12 +2552,14 @@ C471E86728F9BB650021E251 /* OnboardingWindowController.swift in Sources */, C471E86828F9BB650021E251 /* PreferencesWindowController.swift in Sources */, C471E86928F9BB650021E251 /* PreferencesWindowController+Hotkey.swift in Sources */, - C471E86A28F9BB650021E251 /* PrefsVC.swift in Sources */, + C48DDD0F29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, + C4AFC4B029C4F32F00BF4E0D /* BrewFormula.swift in Sources */, + C471E86A28F9BB650021E251 /* PreferencesVC.swift in Sources */, C471E86B28F9BB650021E251 /* PreferenceName.swift in Sources */, C471E86C28F9BB650021E251 /* Preferences.swift in Sources */, - C491998229901E0F001F3A21 /* AppUpdater.swift in Sources */, C4D3660D29113F20006BD146 /* System.swift in Sources */, C471E86D28F9BB650021E251 /* CustomPrefs.swift in Sources */, + C45D654E29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4E2E84C28FC1E70003B070C /* DataExtension.swift in Sources */, C471E86E28F9BB650021E251 /* MenuBarIcons.swift in Sources */, C471E86F28F9BB650021E251 /* Stats.swift in Sources */, @@ -2286,6 +2567,7 @@ C471E87028F9BB650021E251 /* GlobalKeybindPreference.swift in Sources */, C471E87228F9BB650021E251 /* CheckboxPreferenceView.swift in Sources */, C471E87428F9BB650021E251 /* SelectPreferenceView.swift in Sources */, + C490E3B029BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */, C471E87628F9BB650021E251 /* HotkeyPreferenceView.swift in Sources */, C471E87728F9BB650021E251 /* Keys.swift in Sources */, C471E87828F9BB650021E251 /* TerminalProgressWindowController.swift in Sources */, @@ -2308,12 +2590,14 @@ C471E88928F9BB650021E251 /* SwiftUIHelper.swift in Sources */, C471E88B28F9BB650021E251 /* HotKey.swift in Sources */, C471E88C28F9BB650021E251 /* HotKeysController.swift in Sources */, + C41ADCEA2970CCC700120423 /* FSNotifier.swift in Sources */, + C43931CC29C4C03F0069165B /* Brew.swift in Sources */, C471E88D28F9BB650021E251 /* Key.swift in Sources */, C471E88E28F9BB650021E251 /* KeyCombo.swift in Sources */, C471E88F28F9BB650021E251 /* ModifierFlagsExtension.swift in Sources */, C471E7E928F9BAC20021E251 /* Paths.swift in Sources */, C45B91552956123A00F4EC78 /* FakeServicesManager.swift in Sources */, - C471E7FE28F9BACE0021E251 /* HomebrewPackage.swift in Sources */, + C471E7FE28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */, C471E7D828F9BA8F0021E251 /* FileSystemProtocol.swift in Sources */, C471E7F328F9BAC70021E251 /* PhpHelper.swift in Sources */, C471E7E728F9BAC20021E251 /* Constants.swift in Sources */, @@ -2331,30 +2615,37 @@ C471E82628F9BB2E0021E251 /* ComposerJson.swift in Sources */, C471E82428F9BB2E0021E251 /* PhpFrameworks.swift in Sources */, C471E7E828F9BAC20021E251 /* Actions.swift in Sources */, + C40D72612A018AE30054A067 /* BrewFormulaUI.swift in Sources */, C471E82528F9BB2E0021E251 /* ComposerWindow.swift in Sources */, C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */, C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */, C471E82A28F9BB330021E251 /* ValetListable.swift in Sources */, - C471E82728F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */, + C471E82728F9BB310021E251 /* BrewDiagnostics.swift in Sources */, C471E81C28F9BB250021E251 /* BetterAlert.swift in Sources */, C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */, + C490E3B929BCA368006D2DE6 /* App+BrewWatch.swift in Sources */, C471E7FF28F9BAD10021E251 /* Xdebug.swift in Sources */, - C471E7F228F9BAC70021E251 /* PhpEnv.swift in Sources */, + C409349F298EE8E900D25014 /* AppUpdater.swift in Sources */, + C471E7F228F9BAC70021E251 /* PhpEnvironments.swift in Sources */, C471E7E628F9BAC20021E251 /* Process.swift in Sources */, C471E81928F9BAE80021E251 /* NSMenuItemExtension.swift in Sources */, C45B914B295607F400F4EC78 /* Service.swift in Sources */, C471E7D928F9BA8F0021E251 /* TestableShell.swift in Sources */, C471E81428F9BAE80021E251 /* NSWindowExtension.swift in Sources */, + C43BCD4629FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, C471E7D328F9BA8F0021E251 /* ActiveShell.swift in Sources */, + C4B79EC829CA474200A483EE /* FakeCommand.swift in Sources */, C471E7DE28F9BAA30021E251 /* CommandProtocol.swift in Sources */, C471E81B28F9BB250021E251 /* BetterAlertVC.swift in Sources */, C471E82928F9BB330021E251 /* Valet.swift in Sources */, C471E80728F9BAD40021E251 /* PhpConfigurationFile.swift in Sources */, C471E7D528F9BA8F0021E251 /* TestableConfigurations.swift in Sources */, + C436B39F29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, C471E7E328F9BAC20021E251 /* Logger.swift in Sources */, C471E7FD28F9BACE0021E251 /* HomebrewService.swift in Sources */, C471E7E428F9BAC20021E251 /* Helpers.swift in Sources */, C4CB6E67292C362C002E9027 /* Homebrew.swift in Sources */, + C489E0BD2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */, C471E82028F9BB290021E251 /* NginxConfigurationFile.swift in Sources */, C471E7D428F9BA8F0021E251 /* ActiveFileSystem.swift in Sources */, @@ -2363,8 +2654,10 @@ C471E7E528F9BAC20021E251 /* Events.swift in Sources */, C471E7D628F9BA8F0021E251 /* RealFileSystem.swift in Sources */, C471E81728F9BAE80021E251 /* NSMenuExtension.swift in Sources */, + C40D725C2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */, C471E81328F9BAE80021E251 /* XibLoadable.swift in Sources */, C4D3661C291173EA006BD146 /* DictionaryExtension.swift in Sources */, + C4B79ECD29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, C471E7F128F9BAC70021E251 /* VersionNumber.swift in Sources */, C471E7DC28F9BA8F0021E251 /* ShellProtocol.swift in Sources */, ); @@ -2374,13 +2667,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C436B3A029F3C42500B6A64E /* PreferencesTabs.swift in Sources */, C471E89028F9BB8F0021E251 /* AlertableError.swift in Sources */, C471E89128F9BB8F0021E251 /* Errors.swift in Sources */, + C4B79EC929CA474200A483EE /* FakeCommand.swift in Sources */, C471E89228F9BB8F0021E251 /* Alert.swift in Sources */, - C4FD87A529AB98720002D701 /* PhpConfigChecker.swift in Sources */, C471E89328F9BB8F0021E251 /* Application.swift in Sources */, C471E89428F9BB8F0021E251 /* LocalNotification.swift in Sources */, + C40934A5298EEB2C00D25014 /* CaskFile.swift in Sources */, C471E89528F9BB8F0021E251 /* MenuBarImageGenerator.swift in Sources */, + C40D725D2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */, C471E89628F9BB8F0021E251 /* PMWindowController.swift in Sources */, C471E89728F9BB8F0021E251 /* VersionExtractor.swift in Sources */, C47DF1B2299D5A3B0007055D /* LoginItemManager.swift in Sources */, @@ -2392,9 +2688,15 @@ C471E89F28F9BB8F0021E251 /* ValetDomainScanner.swift in Sources */, C471E8A028F9BB8F0021E251 /* FakeDomainScanner.swift in Sources */, C471E8A228F9BB8F0021E251 /* AppDelegate.swift in Sources */, + C43931CD29C4C03F0069165B /* Brew.swift in Sources */, C451AFF92969E40F0078E617 /* HelpButton.swift in Sources */, + C4ACE9E429F84EDD00110766 /* PhpGuard.swift in Sources */, C471E8A328F9BB8F0021E251 /* AppDelegate+MenuOutlets.swift in Sources */, + C4B79EB929CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */, C471E8A428F9BB8F0021E251 /* AppDelegate+Notifications.swift in Sources */, + C490E3B329BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */, + C489E0BE2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, + C490E3B229BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */, C471E8A528F9BB8F0021E251 /* AppDelegate+InterApp.swift in Sources */, C471E8A628F9BB8F0021E251 /* App.swift in Sources */, C471E8A728F9BB8F0021E251 /* App+ActivationPolicy.swift in Sources */, @@ -2405,8 +2707,7 @@ C471E8AB28F9BB8F0021E251 /* EnvironmentCheck.swift in Sources */, C471E8AD28F9BB8F0021E251 /* AppVersion.swift in Sources */, C471E8AE28F9BB8F0021E251 /* ServicesManager.swift in Sources */, - C471E8AF28F9BB8F0021E251 /* EnvironmentManager.swift in Sources */, - C471E8B028F9BB8F0021E251 /* ActivePhpInstallation+Checks.swift in Sources */, + C471E8B028F9BB8F0021E251 /* Valet+Alerts.swift in Sources */, C471E8B128F9BB8F0021E251 /* MainMenu.swift in Sources */, C471E8B228F9BB8F0021E251 /* MainMenu+Startup.swift in Sources */, C471E8B328F9BB8F0021E251 /* MainMenu+Async.swift in Sources */, @@ -2424,6 +2725,7 @@ C4E2E86A28FC3002003B070C /* Utility.swift in Sources */, C471E8BF28F9BB8F0021E251 /* DomainListWindowController.swift in Sources */, C471E8C028F9BB8F0021E251 /* DomainListVC.swift in Sources */, + C4D5576729C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */, C471E8C128F9BB8F0021E251 /* DomainListVC+ContextMenu.swift in Sources */, C4BF56AE2949381100379603 /* FakeValetInteractor.swift in Sources */, C471E8C228F9BB8F0021E251 /* DomainListVC+Actions.swift in Sources */, @@ -2436,10 +2738,12 @@ C471E8C728F9BB8F0021E251 /* Warning.swift in Sources */, C471E8C828F9BB8F0021E251 /* WarningManager.swift in Sources */, C471E8C928F9BB8F0021E251 /* WarningsWindowController.swift in Sources */, + C41ADCEB2970CCC700120423 /* FSNotifier.swift in Sources */, C471E8CA28F9BB8F0021E251 /* OnboardingWindowController.swift in Sources */, + C4D4CB3A29C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */, C471E8CB28F9BB8F0021E251 /* PreferencesWindowController.swift in Sources */, C471E8CC28F9BB8F0021E251 /* PreferencesWindowController+Hotkey.swift in Sources */, - C471E8CD28F9BB8F0021E251 /* PrefsVC.swift in Sources */, + C471E8CD28F9BB8F0021E251 /* PreferencesVC.swift in Sources */, C471E8CE28F9BB8F0021E251 /* PreferenceName.swift in Sources */, C471E8CF28F9BB8F0021E251 /* Preferences.swift in Sources */, C4E2E84D28FC1E70003B070C /* DataExtension.swift in Sources */, @@ -2453,14 +2757,20 @@ C471E8DA28F9BB8F0021E251 /* Keys.swift in Sources */, C471E8DB28F9BB8F0021E251 /* TerminalProgressWindowController.swift in Sources */, C471E8DC28F9BB8F0021E251 /* ProgressVC.swift in Sources */, + C490E3BF29BCA376006D2DE6 /* Measurements.swift in Sources */, C471E8DE28F9BB8F0021E251 /* App+ConfigWatch.swift in Sources */, C471E8DF28F9BB8F0021E251 /* PhpConfigWatcher.swift in Sources */, + C4CB250529B28BB800CA4492 /* MainMenuTest.swift in Sources */, + C40D72622A018AE30054A067 /* BrewFormulaUI.swift in Sources */, + C4B79ECE29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, C471E8E028F9BB8F0021E251 /* Preset.swift in Sources */, C471E8E128F9BB8F0021E251 /* PresetHelper.swift in Sources */, C471E8E228F9BB8F0021E251 /* WarningView.swift in Sources */, C471E8E328F9BB8F0021E251 /* WarningListView.swift in Sources */, C471E8E428F9BB8F0021E251 /* NoWarningsView.swift in Sources */, C471E8E528F9BB8F0021E251 /* OnboardingView.swift in Sources */, + C4B79EBF29CA38DB00A483EE /* BrewCommand.swift in Sources */, + C45D654F29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C471E8E628F9BB8F0021E251 /* VersionPopoverView.swift in Sources */, C471E8E728F9BB8F0021E251 /* NoDomainResultsView.swift in Sources */, C471E8E828F9BB8F0021E251 /* ServicesView.swift in Sources */, @@ -2469,6 +2779,7 @@ C471E8EA28F9BB8F0021E251 /* SectionHeaderView.swift in Sources */, C4D36604291132B7006BD146 /* ValetScanners.swift in Sources */, C471E8EB28F9BB8F0021E251 /* HeaderView.swift in Sources */, + C4FD87A829AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */, C45B9151295608E300F4EC78 /* ValetServicesManager.swift in Sources */, C471E8EC28F9BB8F0021E251 /* SwiftUIHelper.swift in Sources */, C471E8EE28F9BB8F0021E251 /* HotKey.swift in Sources */, @@ -2478,12 +2789,14 @@ C471E8F228F9BB8F0021E251 /* ModifierFlagsExtension.swift in Sources */, C471E7F028F9BAC30021E251 /* Paths.swift in Sources */, C4CE7F9929683B43000102CF /* PhpVersionNumberCollection.swift in Sources */, - C471E7FC28F9BACE0021E251 /* HomebrewPackage.swift in Sources */, + C471E7FC28F9BACE0021E251 /* HomebrewDecodable.swift in Sources */, C471E7CF28F9BA600021E251 /* ActiveShell.swift in Sources */, C4BB393C2981AFC700F8E797 /* PhpVersionSource.swift in Sources */, C471E7F628F9BAC80021E251 /* PhpHelper.swift in Sources */, C471E7EE28F9BAC30021E251 /* Constants.swift in Sources */, + C40934A0298EE8E900D25014 /* AppUpdater.swift in Sources */, C471E80E28F9BAE80021E251 /* DateExtension.swift in Sources */, + C490E3BA29BCA368006D2DE6 /* App+BrewWatch.swift in Sources */, C471E7D028F9BA630021E251 /* FileSystemProtocol.swift in Sources */, C471E81228F9BAE80021E251 /* TimeIntervalExtension.swift in Sources */, C471E7DF28F9BAAB0021E251 /* RealCommand.swift in Sources */, @@ -2494,24 +2807,25 @@ C471E80128F9BAD40021E251 /* ActivePhpInstallation.swift in Sources */, C471E80228F9BAD40021E251 /* PhpInstallation.swift in Sources */, C471E81028F9BAE80021E251 /* StringExtension.swift in Sources */, + C48DDD1029C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, C471E7F828F9BACB0021E251 /* InternalSwitcher.swift in Sources */, C471E82328F9BB2E0021E251 /* ComposerJson.swift in Sources */, - C491997E29901DF7001F3A21 /* CaskFile.swift in Sources */, C471E82128F9BB2E0021E251 /* PhpFrameworks.swift in Sources */, C471E7EF28F9BAC30021E251 /* Actions.swift in Sources */, C471E82228F9BB2E0021E251 /* ComposerWindow.swift in Sources */, C4D3660E29113F20006BD146 /* System.swift in Sources */, C471E80428F9BAD40021E251 /* PhpExtension.swift in Sources */, + C43931C829C4BD610069165B /* PhpFormulaeView.swift in Sources */, C471E7F728F9BACB0021E251 /* PhpSwitcher.swift in Sources */, C4463FCF29804BCB007B93D5 /* RCFile.swift in Sources */, C471E82C28F9BB340021E251 /* ValetListable.swift in Sources */, - C471E82828F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */, + C471E82828F9BB310021E251 /* BrewDiagnostics.swift in Sources */, C471E81E28F9BB260021E251 /* BetterAlert.swift in Sources */, + C43BCD4729FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, C44E985F29B23EBF0059F773 /* UpdateCheckTest.swift in Sources */, C471E7D228F9BA630021E251 /* ActiveFileSystem.swift in Sources */, C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */, - C471E7F528F9BAC80021E251 /* PhpEnv.swift in Sources */, - C491998329901E0F001F3A21 /* AppUpdater.swift in Sources */, + C471E7F528F9BAC80021E251 /* PhpEnvironments.swift in Sources */, C471E7ED28F9BAC30021E251 /* Process.swift in Sources */, C471E81128F9BAE80021E251 /* NSMenuItemExtension.swift in Sources */, C471E7CC28F9BA5B0021E251 /* TestableShell.swift in Sources */, @@ -2528,6 +2842,7 @@ C471E7EB28F9BAC30021E251 /* Helpers.swift in Sources */, C4CB6E68292C362C002E9027 /* Homebrew.swift in Sources */, C4181F1128FAF9330042EA28 /* UITestCase.swift in Sources */, + C4AFC4B129C4F32F00BF4E0D /* BrewFormula.swift in Sources */, C471E81F28F9BB290021E251 /* NginxConfigurationFile.swift in Sources */, C471E7BF28F9B90F0021E251 /* StartupTest.swift in Sources */, C4D3661D291173EA006BD146 /* DictionaryExtension.swift in Sources */, @@ -2552,6 +2867,7 @@ C485707128BF452E00539B36 /* WarningManager.swift in Sources */, C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, C4FACE81288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */, + C40934A3298EEB2C00D25014 /* CaskFile.swift in Sources */, 54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */, C413E43528DA3EB100AE33C7 /* TestableShellTest.swift in Sources */, C4205A7F27F4D21800191A39 /* ValetProxy.swift in Sources */, @@ -2565,6 +2881,7 @@ 54B48B60275F66AE006D90C5 /* Application.swift in Sources */, C4FE011228084FC200D1DE6D /* SelectionVC.swift in Sources */, C4D3661B291173EA006BD146 /* DictionaryExtension.swift in Sources */, + C45D654D29F52F74004C28F9 /* BrewPermissionFixer.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, C493084B279F331F009C240B /* AddSiteVC.swift in Sources */, C44A874928905BB000498BC4 /* ProgressVC.swift in Sources */, @@ -2579,15 +2896,15 @@ C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */, - C491997C29901DF7001F3A21 /* CaskFile.swift in Sources */, - C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, + C41ADCE92970CCC700120423 /* FSNotifier.swift in Sources */, + C40C7F2927721FF600DDDCDC /* Valet+Alerts.swift in Sources */, C485707A28BF457800539B36 /* WarningListView.swift in Sources */, C4C0E8E827F88B41002D32A9 /* DomainScanner.swift in Sources */, C449B4F027EE7FB800C47E8A /* DomainListTLSCell.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* HomebrewPackageTest.swift in Sources */, C485707928BF456C00539B36 /* ArrayExtension.swift in Sources */, - C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, + C4F780CA25D80B75000DBC97 /* HomebrewDecodable.swift in Sources */, C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */, C4F319C927B034A500AFF46F /* Stats.swift in Sources */, C4F30B04278E16BA00755FCE /* HomebrewService.swift in Sources */, @@ -2595,18 +2912,22 @@ C4AF9F7B2754499000D44ED0 /* Valet.swift in Sources */, C4C1019C27C65C6F001FACC2 /* Process.swift in Sources */, C451AFF72969E40F0078E617 /* HelpButton.swift in Sources */, + C490E3B129BC9FE8006D2DE6 /* ProgressViewSubject.swift in Sources */, C47DF1B0299D5A3B0007055D /* LoginItemManager.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, C45B914A295607F400F4EC78 /* Service.swift in Sources */, + C43931C629C4BD610069165B /* PhpFormulaeView.swift in Sources */, C4C0E8E327F88B13002D32A9 /* ValetDomainScanner.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, C463E381284930EE00422731 /* PresetHelper.swift in Sources */, C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */, + C4D5576529C77CC5001A44CD /* PhpVersionManagerWC.swift in Sources */, C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */, C4E49DEB28F7643D0026AC4E /* CommandProtocol.swift in Sources */, - C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, + C4F2E4382752F08D0020E974 /* BrewDiagnostics.swift in Sources */, C485707428BF454E00539B36 /* ServicesView.swift in Sources */, + C4B79EC729CA474200A483EE /* FakeCommand.swift in Sources */, C4F780AE25D80B37000DBC97 /* PhpExtensionTest.swift in Sources */, C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4FC21B128391F8E00D368BB /* MainMenu+Actions.swift in Sources */, @@ -2614,7 +2935,9 @@ C4EED88A27A48778006D7272 /* InterAppHandler.swift in Sources */, C4159AF728E4D40400545349 /* RealShellTest.swift in Sources */, C450C8C728C919EC002A2B4B /* PreferenceName.swift in Sources */, + C40D725B2A018ACC0054A067 /* PhpFormulaeStatus.swift in Sources */, C48D6C75279CD3E400F26D7E /* PhpVersionNumberTest.swift in Sources */, + C40D72602A018AE30054A067 /* BrewFormulaUI.swift in Sources */, C485707B28BF458900539B36 /* VersionPopoverView.swift in Sources */, C4E2E85D28FC282B003B070C /* TestableConfiguration.swift in Sources */, C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */, @@ -2623,16 +2946,16 @@ C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, + C4AFC4B429C4F43300BF4E0D /* HomebrewUpgradableTest.swift in Sources */, C4E2E84828FC1D93003B070C /* TestableConfigurationTest.swift in Sources */, C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */, C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, + C4AFC4AF29C4F32F00BF4E0D /* BrewFormula.swift in Sources */, C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */, - C4FD87A729AB98730002D701 /* PhpConfigChecker.swift in Sources */, C485706D28BF450900539B36 /* NSMenuItemExtension.swift in Sources */, - C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */, + C481F79726164A78004FBCFF /* PreferencesVC.swift in Sources */, C495F5B028A42E080087F70A /* EnvironmentCheck.swift in Sources */, C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, - C491998129901E0F001F3A21 /* AppUpdater.swift in Sources */, C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */, C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, C4AD38B328ECD9D300FA8D83 /* TestableFileSystem.swift in Sources */, @@ -2643,6 +2966,7 @@ C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C4E49DEE28F764A00026AC4E /* TestableCommand.swift in Sources */, C4AF9F78275447F100D44ED0 /* ValetConfigurationTest.swift in Sources */, + C490E3B529BC9FEA006D2DE6 /* ProgressWindowView.swift in Sources */, C40175B92903108900763A68 /* ValetInteractor.swift in Sources */, C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */, C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */, @@ -2658,12 +2982,16 @@ C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4E2E86528FC2F1B003B070C /* XCPMApplication.swift in Sources */, C4E49DE828F764050026AC4E /* ActiveCommand.swift in Sources */, + C489E0BC2A220A4200323F5E /* FakeBrewFormulaeHandler.swift in Sources */, C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */, + C4B79ECC29CA475900A483EE /* RemovePhpVersionCommand.swift in Sources */, + C4FD87AA29AB9ABD0002D701 /* PhpConfigChecker.swift in Sources */, C485707D28BF45A200539B36 /* WarningView.swift in Sources */, C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */, C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */, C4CDA894288F1A71007CE25F /* Keys.swift in Sources */, C4D3660C29113F20006BD146 /* System.swift in Sources */, + C4ACE9E229F84EDD00110766 /* PhpGuard.swift in Sources */, C4D936CA27E3EB6100BD69FE /* PhpHelper.swift in Sources */, C4D36611291140BE006BD146 /* TestableFileSystemTest.swift in Sources */, C45B91542956123A00F4EC78 /* FakeServicesManager.swift in Sources */, @@ -2676,12 +3004,12 @@ C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */, C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */, C44C198E276E3A1C0072762D /* TerminalProgressWindowController.swift in Sources */, + C4B79EBD29CA38DB00A483EE /* BrewCommand.swift in Sources */, C485707828BF456300539B36 /* Warning.swift in Sources */, C415938027A1B54F00D2E1B7 /* PhpFrameworks.swift in Sources */, C40F505628ECA64E004AD45B /* TestableConfigurations.swift in Sources */, C4D9ADC9277611A0007277F4 /* InternalSwitcher.swift in Sources */, C449B4F227EE7FC400C47E8A /* DomainListPhpCell.swift in Sources */, - C491997729901DD6001F3A21 /* CaskFileParserTest.swift in Sources */, C42CFB1A27DFE8BD00862737 /* NginxConfigurationTest.swift in Sources */, C4BF56AC2949381100379603 /* FakeValetInteractor.swift in Sources */, C471E79428F9B23B0021E251 /* FileSystemProtocol.swift in Sources */, @@ -2689,10 +3017,12 @@ C485707C28BF459500539B36 /* NoWarningsView.swift in Sources */, C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */, C40B24F227A310770018C7D2 /* Events.swift in Sources */, + C490E3B829BCA367006D2DE6 /* App+BrewWatch.swift in Sources */, C44AD3F72912EF7100997FF4 /* RealFileSystemTest.swift in Sources */, C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */, C4C0E8E027F88AEB002D32A9 /* FakeDomainScanner.swift in Sources */, C4463FCD29804BCB007B93D5 /* RCFile.swift in Sources */, + C409349E298EE8E900D25014 /* AppUpdater.swift in Sources */, C4AF9F7D275454A900D44ED0 /* ValetVersionExtractorTest.swift in Sources */, C4B56362276AB0A500F12CCB /* VersionExtractorTest.swift in Sources */, C4B585452770FE3900DA4FBE /* RealCommand.swift in Sources */, @@ -2702,29 +3032,36 @@ C4927F0C27B2DFC200C55AFD /* Errors.swift in Sources */, C485707628BF455100539B36 /* SectionHeaderView.swift in Sources */, C46EBC4828DB9644007ACC74 /* RealShell.swift in Sources */, + C48DDD0E29C75C9E00D032D9 /* BlockingOverlayView.swift in Sources */, C4E4404727C56F4700D225E1 /* ValetSite.swift in Sources */, C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */, C48D6C71279CD2AC00F26D7E /* VersionNumber.swift in Sources */, C485706F28BF452300539B36 /* WarningsWindowController.swift in Sources */, C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */, + C43931CB29C4C03F0069165B /* Brew.swift in Sources */, C41C02AB27E61CB3009F26CB /* FakeValetSite.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */, + C4D4CB3829C109CF00DB9F93 /* InternalSwitcher+Valet.swift in Sources */, + C4B79EB729CA387F00A483EE /* BrewFormulaeHandler.swift in Sources */, C4B5853F2770FE3900DA4FBE /* Paths.swift in Sources */, C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */, - C4A6957728D23EE300A14CF8 /* EnvironmentManager.swift in Sources */, C4E0F7EE27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, C4A81CA528C67101008DD9D1 /* PMTableView.swift in Sources */, C45E76152854A65300B4FE0C /* ServicesManager.swift in Sources */, C4D36602291132B7006BD146 /* ValetScanners.swift in Sources */, + C40934AB298EEDA900D25014 /* CaskFileParserTest.swift in Sources */, + C436B39E29F3C42500B6A64E /* PreferencesTabs.swift in Sources */, + C43BCD4529FBEF40001547BC /* InstallAndUpgradeCommand.swift in Sources */, C4551657297AED18009B8466 /* ValetRcTest.swift in Sources */, C464ADAD275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */, - C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */, + C40C7F1F2772136000DDDCDC /* PhpEnvironments.swift in Sources */, C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */, C43A8A1A25D9CD1000591B77 /* Utility.swift in Sources */, C46EBC4B28DB966A007ACC74 /* TestableShell.swift in Sources */, C40FE73B282ABB2E00A302C2 /* AppVersionTest.swift in Sources */, + C490E3BD29BCA375006D2DE6 /* Measurements.swift in Sources */, C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2761,6 +3098,142 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + C406A5FD298AD2CF00B5B85A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C406A5FE298AD2CF00B5B85A /* Debug.Dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug.Dev; + }; + C406A5FF298AD2CF00B5B85A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + C406A600298AD2CF00B5B85A /* Release.Dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release.Dev; + }; C41C1B4122B0098000E7CF16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2889,7 +3362,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1079; + CURRENT_PROJECT_VERSION = 1250; DEAD_CODE_STRIPPING = YES; DEBUG = YES; DEVELOPMENT_TEAM = 8M54J5J787; @@ -2902,7 +3375,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2919,7 +3392,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1079; + CURRENT_PROJECT_VERSION = 1250; DEAD_CODE_STRIPPING = YES; DEBUG = NO; DEVELOPMENT_TEAM = 8M54J5J787; @@ -2932,7 +3405,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2946,10 +3419,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2964,10 +3438,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2982,10 +3457,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3000,10 +3476,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3018,9 +3495,10 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3036,9 +3514,10 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3054,9 +3533,10 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3072,9 +3552,10 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3119,6 +3600,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Mac Developer"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -3148,7 +3630,8 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1079; + CURRENT_PROJECT_VERSION = 1250; + DEAD_CODE_STRIPPING = YES; DEBUG = NO; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -3160,7 +3643,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; PRODUCT_NAME = "$(TARGET_NAME) DEV"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3173,6 +3656,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; @@ -3181,7 +3665,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3223,6 +3707,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Mac Developer"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -3259,7 +3744,8 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1079; + CURRENT_PROJECT_VERSION = 1250; + DEAD_CODE_STRIPPING = YES; DEBUG = YES; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -3271,9 +3757,9 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.4; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.dev; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "$(TARGET_NAME) DEV"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -3284,6 +3770,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 8M54J5J787; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ""; @@ -3292,13 +3779,378 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug.Dev; }; + C4E9D8EF29CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug.EA; + }; + C4E9D8F029CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconEAP; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1250; + DEAD_CODE_STRIPPING = YES; + DEBUG = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = phpmon/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor EAP"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 6.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; + PRODUCT_NAME = "$(TARGET_NAME) EAP"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Debug.EA; + }; + C4E9D8F129CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug.EA; + }; + C4E9D8F229CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug.EA; + }; + C4E9D8F329CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Debug.EA; + }; + C4E9D8F429CB9A6400BD28D4 /* Debug.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "PHP Monitor"; + }; + name = Debug.EA; + }; + C4E9D8F529CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release.EA; + }; + C4E9D8F629CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconEAP; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AppColor; + CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1250; + DEAD_CODE_STRIPPING = YES; + DEBUG = NO; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + INFOPLIST_FILE = phpmon/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor EAP"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 6.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap; + PRODUCT_NAME = "$(TARGET_NAME) EAP"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release.EA; + }; + C4E9D8F729CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "phpmon-updater/phpmon-updater.entitlements"; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 30; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + ENABLE_HARDENED_RUNTIME = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "PHP Monitor Self-Updater"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Nico Verbruggen. All rights reserved."; + INFOPLIST_KEY_NSPrincipalClass = NSApplication; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon-updater"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release.EA; + }; + C4E9D8F829CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release.EA; + }; + C4E9D8F929CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ""; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.feature-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Release.EA; + }; + C4E9D8FA29CB9A7200BD28D4 /* Release.EA */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = 8M54J5J787; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.ui-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "PHP Monitor"; + }; + name = Release.EA; + }; C4F7808125D7F84B000DBC97 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3313,7 +4165,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3334,7 +4186,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 11.0; + MACOSX_DEPLOYMENT_TARGET = 12.4; PRODUCT_BUNDLE_IDENTIFIER = "com.nicoverbruggen.phpmon.unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -3344,13 +4196,28 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + C406A5FC298AD2CF00B5B85A /* Build configuration list for PBXNativeTarget "PHP Monitor Self-Updater" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C406A5FD298AD2CF00B5B85A /* Debug */, + C406A5FE298AD2CF00B5B85A /* Debug.Dev */, + C4E9D8F129CB9A6400BD28D4 /* Debug.EA */, + C406A5FF298AD2CF00B5B85A /* Release */, + C406A600298AD2CF00B5B85A /* Release.Dev */, + C4E9D8F729CB9A7200BD28D4 /* Release.EA */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C41C1B2E22B0097F00E7CF16 /* Build configuration list for PBXProject "PHP Monitor" */ = { isa = XCConfigurationList; buildConfigurations = ( C41C1B4122B0098000E7CF16 /* Debug */, C4975D0A28CD193A00FFB4E8 /* Debug.Dev */, + C4E9D8EF29CB9A6400BD28D4 /* Debug.EA */, C41C1B4222B0098000E7CF16 /* Release */, C4975D0728CD190C00FFB4E8 /* Release.Dev */, + C4E9D8F529CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3360,8 +4227,10 @@ buildConfigurations = ( C41C1B4422B0098000E7CF16 /* Debug */, C4975D0B28CD193A00FFB4E8 /* Debug.Dev */, + C4E9D8F029CB9A6400BD28D4 /* Debug.EA */, C41C1B4522B0098000E7CF16 /* Release */, C4975D0828CD190C00FFB4E8 /* Release.Dev */, + C4E9D8F629CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3371,8 +4240,10 @@ buildConfigurations = ( C471E7B428F9B4940021E251 /* Debug */, C471E7B528F9B4940021E251 /* Debug.Dev */, + C4E9D8F329CB9A6400BD28D4 /* Debug.EA */, C471E7B628F9B4940021E251 /* Release */, C471E7B728F9B4940021E251 /* Release.Dev */, + C4E9D8F929CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3382,8 +4253,10 @@ buildConfigurations = ( C471E7C528F9B90F0021E251 /* Debug */, C471E7C628F9B90F0021E251 /* Debug.Dev */, + C4E9D8F429CB9A6400BD28D4 /* Debug.EA */, C471E7C728F9B90F0021E251 /* Release */, C471E7C828F9B90F0021E251 /* Release.Dev */, + C4E9D8FA29CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -3393,8 +4266,10 @@ buildConfigurations = ( C4F7808125D7F84B000DBC97 /* Debug */, C4975D0C28CD193A00FFB4E8 /* Debug.Dev */, + C4E9D8F229CB9A6400BD28D4 /* Debug.EA */, C4F7808225D7F84B000DBC97 /* Release */, C4975D0928CD190C00FFB4E8 /* Release.Dev */, + C4E9D8F829CB9A7200BD28D4 /* Release.EA */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme index db36ef9..2e915fa 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor DEV.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme new file mode 100644 index 0000000..4aa04cf --- /dev/null +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor EAP.xcscheme @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor Self-Updater.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor Self-Updater.xcscheme new file mode 100644 index 0000000..f2e4baa --- /dev/null +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor Self-Updater.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme index b7409be..47d77b1 100644 --- a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme @@ -1,6 +1,6 @@ **Note** > If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️) -Backports are available via [this tap](https://github.com/shivammathur/homebrew-php). For more information about those backports, please see the next FAQ entry. +Backports that are installable via PHP Monitor's **PHP Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php). For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet. -
@@ -113,37 +114,35 @@ For maximum compatibility with older PHP versions, you may wish to keep using Va Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2. -You can install other supported versions of PHP out of the box, so `php@8.0` and `php@8.1` at the time of writing. +You can install other supported versions of PHP via PHP Monitor's **PHP Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.) -If you wish to install older (officially unsupported) versions of PHP for local use, you can do so by using [Shivam Mathur's tap](https://github.com/shivammathur/homebrew-php): +Please keep in mind that installing or updating PHP versions, even when done via PHP Monitor's **PHP Manager**, may cause other required formula dependencies (required software needed to keep those PHP versions functional) to be upgraded. It might not be very transparent when this happens, but this is likely the cause if installing a PHP version takes longer than expected: usually other dependencies are also being installed. -```sh -brew tap shivammathur/php -``` +Additionally, upgrading one specific version of PHP may also cause other installed versions of PHP to *also* be updated in one go, if the dependencies for that one version also apply to the other (newer) version(s) of PHP. It's a bit tricky to manage PHP versions via Homebrew, and even PHP Monitor may encounter some difficulties. -You may find that this tap is already in use: if you've used Valet before, it automatically uses this tap for legacy versions of PHP. +If you encounter a strange scenario or a malfunction, please open an issue on the issue tracker and get in touch. I'd like to keep enhancing this process to make it as foolproof as possible. -```sh -brew install shivammathur/php/php@7.4 -brew install shivammathur/php/php@7.3 -brew install shivammathur/php/php@7.2 -brew install shivammathur/php/php@7.1 -brew install shivammathur/php/php@7.0 -``` - -**Always make sure to restart PHP Monitor after installing or upgrading PHP versions!** - -> *Note*: Using this tap may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases. +> *Note*: Using PHP Monitor when managing PHP versions may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
I want PHP Monitor to start up when I boot my Mac! -You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account. +If you are running macOS Ventura or newer, there's an option in the Settings menu that you can select: "Start PHP Monitor at login". + +If you are on an older version of macOS, you can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account. Super convenient!
+
+What features are unavailable in Standalone Mode? + +The services manager is disabled, and all other obvious Laravel Valet integrations (configuration finder, domains list, Fix My Valet) are also disabled. + +(Most other features remain available.) +
+
I want to set up PHP Monitor from scratch! I don't have Homebrew installed either, where do I begin? @@ -170,7 +169,7 @@ If you're on an Apple Silicon-based Mac, you'll need to add: and add the following to your `.zshrc` file, but add this BEFORE the homebrew PATH additions: export PATH=$HOME/bin:~/.composer/vendor/bin:$PATH - + If you're adding `composer` and Homebrew binaries, ensure that Homebrew binaries are preferred by adding these to the path last. On my system, that looks like this: export PATH=$HOME/bin:/usr/local/bin:$PATH @@ -189,8 +188,12 @@ Make sure PHP is linked correctly: should return: `/usr/local/bin/php` (or `/opt/homebrew/bin/php` if you are on Apple Silicon) +**If you don't need Laravel Valet, you can stop here. PHP Monitor will work like this in Standalone Mode.** + +If you'd like to have Valet as well, continue and install Valet with Composer, like this. + composer global require laravel/valet - + For optimal results, you should lock your PHP platform for global dependencies to the oldest version of PHP you intend to run. If that version is PHP 7.0, your `~/.composer/composer.json` file could look like this (please adjust the version accordingly!): ``` @@ -209,18 +212,13 @@ For optimal results, you should lock your PHP platform for global dependencies t Run `composer global update` again. This ensures that when you switch to a different global PHP version, [Valet won't break](https://github.com/nicoverbruggen/phpmon/issues/178). If it does, PHP Monitor will let you know what you can do about this. Then, install Valet: - + valet install This should install `dnsmasq` and set up Valet. Great, almost there! valet trust -You can now install PHP Monitor, if you haven't already: - - brew tap nicoverbruggen/homebrew-cask - brew install --cask phpmon - Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. You will need to approve the initial launch of the app, but you should be ready to go now.
@@ -229,13 +227,17 @@ Finally, run PHP Monitor. Since the app is notarized and signed with a developer PHP Monitor will check if an update is available every time you start the app. -You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually. +You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". (You can always check for updates manually.)
I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this? +The easiest way is to simply use the built-in **PHP Version Manager**, which will allow you to upgrade your PHP versions with one click. + +If you want to do this manually, you can follow the instructions below. + It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor. Here's what I usually do: @@ -265,7 +267,7 @@ This should resolve the issue! If that does not fix the issue, run `brew link ph brew install php brew link php --force - +
@@ -320,12 +322,14 @@ Make sure you have at least **Valet 3.0** installed, since support for isolation
One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark! -The value you provided in your INI file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!) +The value you provided in your `.ini` file is invalid. If that is the case, PHP will attempt to parse your value as bytes, which is usually unintended. (`1GB` will resolve to merely a few bytes, and all of your applications will run out of memory!) You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` is also allowed, or just an integer (which will result in N amount of bytes being the limit). **Example**: Trying to use `1GB` as the memory limit, for example, will result in this exclamation mark. The correct way to set a 1GB limit is by using `1G` as the value. (Note: The displayed value will append `B` for clarity, so if you set `1G`, the value reported by PHP Monitor will be 1 GB.) +(If you are using Valet, you can adjust these limits in the `.conf.d/php-memory-limits.ini` file. Otherwise, you may need to adjust `php.ini`.) +
@@ -414,6 +418,9 @@ You can omit the `php` key in the preset if you do not wish for the preset to sw
How do I ensure additional Homebrew services are shown in the app? +> **Info** +> Homebrew services aren't displayed if you are using Valet in Standalone Mode. + You must set these services up in a JSON file, located in `~/.config/phpmon/config.json`. You can specify custom services in the configuration file for Homebrew services that run as your own user (not root). @@ -594,9 +601,9 @@ Thank you very much for your contributions, kind words and support. ### Loading info about PHP in the background -This utility runs `php-config --version` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). +This app runs `php-config --version` in the background periodically, usually whenever your Homebrew configuration is modified. A filesystem watcher is used to determine if anything changes in your Homebrew's `bin` directory. -In order to save power, this only happens once every 60 seconds. +PHP Monitor also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). See also the section on *Config change detection* below. ### Switching PHP versions @@ -604,7 +611,7 @@ This utility will detect which PHP versions you have installed via Homebrew, and The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valet’s switcher. -If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed. +If you're using Valet 3 or newer, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed. ### Config change detection diff --git a/SECURITY.md b/SECURITY.md index 04bfa7f..561b5ab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc | Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- -| 5.8 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)
Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x)
PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended
2.16.2 minimum | +| 6.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)
Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x)
PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended
2.16.2 minimum | ## Legacy versions @@ -14,7 +14,8 @@ These versions of PHP Monitor are no longer supported, but if you’re using an | Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- -| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)
Monterey (12.0)
Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended
2.16.2 minimum | +| 5.8 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)
Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x)
PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended
2.16.2 minimum | +| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)
Monterey (12.0)
Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x)
PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended
2.16.2 minimum | | 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)
Monterey (12.0)
Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)
PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended
2.16.2 minimum | | 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)
Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)
Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 | diff --git a/assets/affinity/icon_se.afdesign b/assets/affinity/icon-dev.afdesign similarity index 100% rename from assets/affinity/icon_se.afdesign rename to assets/affinity/icon-dev.afdesign diff --git a/assets/affinity/icon-eap.afdesign b/assets/affinity/icon-eap.afdesign new file mode 100644 index 0000000..08d7dee Binary files /dev/null and b/assets/affinity/icon-eap.afdesign differ diff --git a/assets/affinity/icon-updater.afdesign b/assets/affinity/icon-updater.afdesign new file mode 100644 index 0000000..7dacd66 Binary files /dev/null and b/assets/affinity/icon-updater.afdesign differ diff --git a/docs/screenshot.jpg b/docs/screenshot.jpg index 4ab45d5..dcc5a74 100644 Binary files a/docs/screenshot.jpg and b/docs/screenshot.jpg differ diff --git a/phpmon-updater/Assets.xcassets/AccentColor.colorset/Contents.json b/phpmon-updater/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/phpmon-updater/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/Contents.json b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..64dc11e --- /dev/null +++ b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 0000000..20350c0 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..18a5b1e Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 0000000..cca963a Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..74732f3 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 0000000..18a5b1e Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..d7707c5 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 0000000..74732f3 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..2c3e1f9 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 0000000..d7707c5 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..bef4d85 Binary files /dev/null and b/phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/phpmon-updater/Assets.xcassets/Contents.json b/phpmon-updater/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/phpmon-updater/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon-updater/LaunchControl.swift b/phpmon-updater/LaunchControl.swift new file mode 100644 index 0000000..deef6ac --- /dev/null +++ b/phpmon-updater/LaunchControl.swift @@ -0,0 +1,46 @@ +// +// LaunchControl.swift +// PHP Monitor Self-Updater +// +// Created by Nico Verbruggen on 02/02/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +class LaunchControl { + public static func smartRestart(priority: [String]) async { + for appPath in priority { + if FileManager.default.fileExists(atPath: appPath) { + let app = await LaunchControl.startApplication(at: appPath) + if app != nil { + return + } + } + } + } + + public static func terminateApplications(bundleIds: [String]) async { + let runningApplications = NSWorkspace.shared.runningApplications + + // Terminate all instances found + for id in bundleIds { + if let phpmon = runningApplications.first(where: { + (application) in return application.bundleIdentifier == id + }) { + phpmon.terminate() + } + } + } + + public static func startApplication(at path: String) async -> NSRunningApplication? { + await withCheckedContinuation { continuation in + let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL + let configuration = NSWorkspace.OpenConfiguration() + NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in + continuation.resume(returning: phpmon) + } + } + } +} diff --git a/phpmon-updater/Updater.swift b/phpmon-updater/Updater.swift new file mode 100644 index 0000000..d67924e --- /dev/null +++ b/phpmon-updater/Updater.swift @@ -0,0 +1,162 @@ +// +// Updater.swift +// PHP Monitor Updater +// +// Created by Nico Verbruggen on 01/02/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +class Updater: NSObject, NSApplicationDelegate { + + var updaterDirectory: String = "" + var manifestPath: String = "" + var manifest: ReleaseManifest! = nil + + func applicationDidFinishLaunching(_ aNotification: Notification) { + Task { await self.installUpdate() } + } + + func installUpdate() async { + print("PHP MONITOR SELF-UPDATER by Nico Verbruggen") + print("===========================================") + + self.updaterDirectory = "~/.config/phpmon/updater" + .replacingOccurrences(of: "~", with: NSHomeDirectory()) + + print("Updater directory set to: \(self.updaterDirectory)") + + self.manifestPath = "\(updaterDirectory)/update.json" + + // Fetch the manifest on the local filesystem + let manifest = await parseManifest()! + + // Download the latest file + let zipPath = await download(manifest) + + // Terminate all instances of PHP Monitor first + await LaunchControl.terminateApplications(bundleIds: [ + "com.nicoverbruggen.phpmon.eap", + "com.nicoverbruggen.phpmon.dev", + "com.nicoverbruggen.phpmon" + ]) + + // Install the app based on the zip + let appPath = await extractAndInstall(zipPath: zipPath) + + // Restart PHP Monitor, this will also close the updater + _ = await LaunchControl.startApplication(at: appPath) + + exit(1) + } + + func applicationWillTerminate(_ aNotification: Notification) { + exit(1) + } + + func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return false + } + + private func parseManifest() async -> ReleaseManifest? { + // Read out the correct information from the manifest JSON + print("Checking manifest file at \(manifestPath)...") + + do { + let manifestText = try String(contentsOfFile: manifestPath) + manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!) + return manifest + } catch { + print("Parsing the manifest failed (or the manifest file doesn't exist)!") + await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.") + } + + return nil + } + + private func download(_ manifest: ReleaseManifest) async -> String { + // Remove all zips + system_quiet("rm -rf \(updaterDirectory)/*.zip") + + // Download the file (and follow redirects + no output on failure) + system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20") + + // Identify the downloaded file + let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip") + .trimmingCharacters(in: .whitespacesAndNewlines) + + // Ensure the zip exists + if filename.isEmpty { + print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!") + await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)") + } + + // Calculate the checksum for the downloaded file + let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'") + .trimmingCharacters(in: .whitespacesAndNewlines) + + // Compare the checksums + print(""" + Comparing checksums... + Expected SHA256: \(manifest.sha256) + Actual SHA256: \(checksum) + """) + + // Make sure the checksum matches before we do anything with the file + if checksum != manifest.sha256 { + print("The checksums failed to match. Cancelling!") + await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.") + } + + // Return the path to the zip + return "\(updaterDirectory)/\(filename)" + } + + private func extractAndInstall(zipPath: String) async -> String { + // Remove the directory that will contain the extracted update + system_quiet("rm -rf \"\(updaterDirectory)/extracted\"") + + // Recreate the directory where we will unzip the .app file + system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"") + + // Make sure the updater directory exists + var isDirectory: ObjCBool = true + if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) { + await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.") + } + + // Unzip the file + system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"") + + // Find the .app file + let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app") + .trimmingCharacters(in: .whitespacesAndNewlines) + + print("Finished extracting: \(updaterDirectory)/extracted/\(app)") + + // Make sure the file was extracted + if app.isEmpty { + await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.") + } + + // Remove the original app + print("Removing \(app) before replacing...") + system_quiet("rm -rf \"/Applications/\(app)\"") + + // Move the new app in place + system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"") + + // Remove the zip + system_quiet("rm \"\(zipPath)\"") + + // Remove the manifest + system_quiet("rm \"\(manifestPath)\"") + + // Write a file that is only written when we upgraded successfully + system_quiet("touch \"\(updaterDirectory)/upgrade.success\"") + + // Return the new location of the app + return "/Applications/\(app)" + } +} diff --git a/phpmon-updater/Utility.swift b/phpmon-updater/Utility.swift new file mode 100644 index 0000000..e980ba6 --- /dev/null +++ b/phpmon-updater/Utility.swift @@ -0,0 +1,34 @@ +// +// Utility.swift +// PHP Monitor Self-Updater +// +// Created by Nico Verbruggen on 02/02/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa + +class Alert { + public static func show(description: String, shouldExit: Bool = true) async { + await withUnsafeContinuation { continuation in + DispatchQueue.main.async { + let alert = NSAlert() + alert.messageText = "The app could not be updated." + alert.informativeText = description + alert.addButton(withTitle: "OK") + alert.alertStyle = .critical + alert.runModal() + if shouldExit { + exit(0) + } + continuation.resume() + } + } + } +} + +public struct ReleaseManifest: Codable { + let url: String + let sha256: String +} diff --git a/phpmon-updater/main.swift b/phpmon-updater/main.swift new file mode 100644 index 0000000..6008aa0 --- /dev/null +++ b/phpmon-updater/main.swift @@ -0,0 +1,14 @@ +// +// AppDelegate.swift +// PHP Monitor Self-Updater +// +// Created by Nico Verbruggen on 01/02/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +let app = NSApplication.shared +let delegate = Updater() +app.delegate = delegate +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/phpmon-updater/phpmon-updater.entitlements b/phpmon-updater/phpmon-updater.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/phpmon-updater/phpmon-updater.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/Contents.json b/phpmon/Assets.xcassets/AppIconEAP.appiconset/Contents.json new file mode 100644 index 0000000..64dc11e --- /dev/null +++ b/phpmon/Assets.xcassets/AppIconEAP.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "icon_16x16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_16x16@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_32x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_32x32@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_128x128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_128x128@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_256x256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_256x256@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_512x512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_512x512@2x.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png new file mode 100644 index 0000000..70a117e Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png new file mode 100644 index 0000000..406c60c Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png new file mode 100644 index 0000000..21adfd2 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png new file mode 100644 index 0000000..dbaa386 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png new file mode 100644 index 0000000..406c60c Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png new file mode 100644 index 0000000..7028370 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png new file mode 100644 index 0000000..dbaa386 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png new file mode 100644 index 0000000..56a9771 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png new file mode 100644 index 0000000..7028370 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png differ diff --git a/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png new file mode 100644 index 0000000..d9d21b6 Binary files /dev/null and b/phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png differ diff --git a/phpmon/Assets.xcassets/StatusColorBlue.colorset/Contents.json b/phpmon/Assets.xcassets/StatusColorBlue.colorset/Contents.json new file mode 100644 index 0000000..4fc8e42 --- /dev/null +++ b/phpmon/Assets.xcassets/StatusColorBlue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.580", + "red" : "0.278" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.988", + "green" : "0.723", + "red" : "0.277" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/phpmon/Common/Command/CommandProtocol.swift b/phpmon/Common/Command/CommandProtocol.swift index 9784ae8..0fe4a79 100644 --- a/phpmon/Common/Command/CommandProtocol.swift +++ b/phpmon/Common/Command/CommandProtocol.swift @@ -16,7 +16,26 @@ protocol CommandProtocol { - Parameter path: The path of the command or program to invoke. - Parameter arguments: A list of arguments that are passed on. - Parameter trimNewlines: Removes empty new line output. + - Parameter withStandardError: Outputs standard error output to the same string output as well. */ - func execute(path: String, arguments: [String], trimNewlines: Bool) -> String + func execute( + path: String, + arguments: [String], + trimNewlines: Bool, + withStandardError: Bool + ) -> String + + /** + Immediately executes a command. + + - Parameter path: The path of the command or program to invoke. + - Parameter arguments: A list of arguments that are passed on. + - Parameter trimNewlines: Removes empty new line output. + */ + func execute( + path: String, + arguments: [String], + trimNewlines: Bool + ) -> String } diff --git a/phpmon/Common/Command/RealCommand.swift b/phpmon/Common/Command/RealCommand.swift index 36cde64..11be535 100644 --- a/phpmon/Common/Command/RealCommand.swift +++ b/phpmon/Common/Command/RealCommand.swift @@ -9,13 +9,23 @@ import Cocoa public class RealCommand: CommandProtocol { - public func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String { + public func execute( + path: String, + arguments: [String], + trimNewlines: Bool, + withStandardError: Bool + ) -> String { let task = Process() task.launchPath = path task.arguments = arguments let pipe = Pipe() task.standardOutput = pipe + + if withStandardError { + task.standardError = pipe + } + task.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() @@ -30,4 +40,17 @@ public class RealCommand: CommandProtocol { return output } + public func execute( + path: String, + arguments: [String], + trimNewlines: Bool = false + ) -> String { + self.execute( + path: path, + arguments: arguments, + trimNewlines: trimNewlines, + withStandardError: false + ) + } + } diff --git a/phpmon/Common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift index 8abc354..b62bdf8 100644 --- a/phpmon/Common/Core/Actions.swift +++ b/phpmon/Common/Core/Actions.swift @@ -12,37 +12,43 @@ class Actions { // MARK: - Services + public static func linkPhp() async { + await brew("link php --overwrite --force") + + // TODO: Verify that this worked, if not, notify the user + } + public static func restartPhpFpm() async { - await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) + await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated) } public static func restartNginx() async { - await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) + await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated) } public static func restartDnsMasq() async { - await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated) + await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated) } public static func stopValetServices() async { - await brew("services stop \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) - await brew("services stop \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) - await brew("services stop \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated) + await brew("services stop \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated) + await brew("services stop \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated) + await brew("services stop \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated) } public static func fixHomebrewPermissions() throws { var servicesCommands = [ - "\(Paths.brew) services stop \(Homebrew.Formulae.nginx)", - "\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)" + "\(Paths.brew) services stop \(HomebrewFormulae.nginx)", + "\(Paths.brew) services stop \(HomebrewFormulae.dnsmasq)" ] var cellarCommands = [ - "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)", - "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)" + "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.nginx)", + "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(HomebrewFormulae.dnsmasq)" ] - PhpEnv.shared.availablePhpVersions.forEach { version in - let formula = version == PhpEnv.brewPhpAlias + PhpEnvironments.shared.availablePhpVersions.forEach { version in + let formula = version == PhpEnvironments.brewPhpAlias ? "php" : "php@\(version)" servicesCommands.append("\(Paths.brew) services stop \(formula)") @@ -119,9 +125,9 @@ class Actions { extensions and/or run `composer global update`. */ public static func fixMyValet() async { - await InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias) - await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated) - await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) - await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) + await InternalSwitcher().performSwitch(to: PhpEnvironments.brewPhpAlias) + await brew("services restart \(HomebrewFormulae.dnsmasq)", sudo: HomebrewFormulae.dnsmasq.elevated) + await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated) + await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated) } } diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index 96b5e26..8fbef99 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -82,6 +82,14 @@ struct Constants { string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb" )! + static let EarlyAccessCaskFile = URL( + string: "https://phpmon.app/builds/early-access/sponsors/phpmon-eap.rb" + )! + + static let EarlyAccessChangelog = URL( + string: "https://phpmon.app/early-access/release-notes" + )! + } } diff --git a/phpmon/Common/Core/Helpers.swift b/phpmon/Common/Core/Helpers.swift index e35af77..bd2a7f8 100644 --- a/phpmon/Common/Core/Helpers.swift +++ b/phpmon/Common/Core/Helpers.swift @@ -8,13 +8,6 @@ // MARK: Common Shell Commands -/** - Runs a `valet` command. Defaults to running as superuser. - */ -func valet(_ command: String, sudo: Bool = true) async -> String { - return await Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)").out -} - /** Runs a `brew` command. Can run as superuser. */ diff --git a/phpmon/Common/Core/Homebrew.swift b/phpmon/Common/Core/Homebrew.swift index de96a64..57add14 100644 --- a/phpmon/Common/Core/Homebrew.swift +++ b/phpmon/Common/Core/Homebrew.swift @@ -8,31 +8,27 @@ import Foundation -class Homebrew { - static var fake: Bool = false - - struct Formulae { - static var php: HomebrewFormula { - if Homebrew.fake { - return HomebrewFormula("php", elevated: true) - } - - if PhpEnv.shared.homebrewPackage == nil { - fatalError("You must either load the HomebrewPackage object or call `fake` on the Homebrew class.") - } - - return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true) +struct HomebrewFormulae { + static var php: HomebrewFormula { + if PhpEnvironments.shared.homebrewPackage == nil { + return HomebrewFormula("php", elevated: true) } - static var nginx: HomebrewFormula { - return HomebrewDiagnostics.usesNginxFullFormula - ? HomebrewFormula("nginx-full", elevated: true) - : HomebrewFormula("nginx", elevated: true) + guard let install = PhpEnvironments.phpInstall else { + return HomebrewFormula("php", elevated: true) } - static var dnsmasq: HomebrewFormula { - return HomebrewFormula("dnsmasq", elevated: true) - } + return HomebrewFormula(install.formula, elevated: true) + } + + static var nginx: HomebrewFormula { + return BrewDiagnostics.usesNginxFullFormula + ? HomebrewFormula("nginx-full", elevated: true) + : HomebrewFormula("nginx", elevated: true) + } + + static var dnsmasq: HomebrewFormula { + return HomebrewFormula("dnsmasq", elevated: true) } } diff --git a/phpmon/Common/Core/Logger.swift b/phpmon/Common/Core/Logger.swift index 8bdb4c8..c7f331f 100644 --- a/phpmon/Common/Core/Logger.swift +++ b/phpmon/Common/Core/Logger.swift @@ -13,6 +13,7 @@ class Log { static var shared = Log() var logFilePath = "~/.config/phpmon/last_session.log" + var logExists = false enum Verbosity: Int { @@ -29,9 +30,9 @@ class Log { public func prepareLogFile() { if !isRunningTests && Verbosity.cli.isApplicable() { - _ = system("mkdir -p ~/.config/phpmon 2> /dev/null") - _ = system("rm ~/.config/phpmon/last_session.log 2> /dev/null") - _ = system("touch ~/.config/phpmon/last_session.log 2> /dev/null") + system_quiet("mkdir -p ~/.config/phpmon 2> /dev/null") + system_quiet("rm ~/.config/phpmon/last_session.log 2> /dev/null") + system_quiet("touch ~/.config/phpmon/last_session.log 2> /dev/null") self.logExists = FileSystem.fileExists(self.logFilePath) } } @@ -72,6 +73,12 @@ class Log { } } + static func line(as verbosity: Verbosity = .info) { + if verbosity.isApplicable() { + Log.shared.log("----------------------------------") + } + } + private func log(_ text: String) { print(text) diff --git a/phpmon/Common/Core/Paths.swift b/phpmon/Common/Core/Paths.swift index e1287b5..fffecb5 100644 --- a/phpmon/Common/Core/Paths.swift +++ b/phpmon/Common/Core/Paths.swift @@ -19,9 +19,19 @@ public class Paths { private var userName: String init() { + // Assume the default directory is correct baseDir = App.architecture != "x86_64" ? .opt : .usr + + // Ensure that if a different location is used, it takes precendence + if baseDir == .usr + && FileSystem.directoryExists("/usr/local/homebrew") + && !FileSystem.directoryExists("/usr/local/Cellar") { + Log.warn("Using /usr/local/homebrew as base directory!") + baseDir = .usr_hb + } + userName = identity() - Log.info("[ID] The current username is `\(userName)`.") + Log.info("The current username is `\(userName)`.") } public func detectBinaryPaths() { @@ -100,6 +110,8 @@ public class Paths { Paths.composer = "/usr/local/bin/composer" } else if FileSystem.fileExists("/opt/homebrew/bin/composer") { Paths.composer = "/opt/homebrew/bin/composer" + } else if FileSystem.fileExists("/usr/local/homebrew/bin/composer") { + Paths.composer = "/usr/local/homebrew/bin/composer" } else { Paths.composer = nil Log.warn("Composer was not found.") @@ -111,6 +123,7 @@ public class Paths { public enum HomebrewDir: String { case opt = "/opt/homebrew" case usr = "/usr/local" + case usr_hb = "/usr/local/homebrew" } } diff --git a/phpmon/Common/Helpers/Alert.swift b/phpmon/Common/Helpers/Alert.swift index bc6c8c1..64809e2 100644 --- a/phpmon/Common/Helpers/Alert.swift +++ b/phpmon/Common/Helpers/Alert.swift @@ -14,6 +14,7 @@ class Alert { messageText: String, informativeText: String, buttonTitle: String = "generic.ok".localized, + buttonIsDestructive: Bool = false, secondButtonTitle: String = "generic.cancel".localized, style: NSAlert.Style = .warning, onFirstButtonPressed: @escaping (() -> Void) @@ -27,6 +28,7 @@ class Alert { alert.messageText = messageText alert.informativeText = informativeText alert.addButton(withTitle: buttonTitle) + alert.buttons.first?.hasDestructiveAction = buttonIsDestructive if !secondButtonTitle.isEmpty { alert.addButton(withTitle: secondButtonTitle) } diff --git a/phpmon/Common/Helpers/FSNotifier.swift b/phpmon/Common/Helpers/FSNotifier.swift new file mode 100644 index 0000000..349d78e --- /dev/null +++ b/phpmon/Common/Helpers/FSNotifier.swift @@ -0,0 +1,69 @@ +// +// FSNotifier.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 13/01/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +class FSNotifier { + enum Kind { + case homebrewLocks, homebrewBinaries + } + + public static var shared: FSNotifier! = nil + + let queue = DispatchQueue(label: "FSWatch2Queue", attributes: .concurrent) + var lastUpdate: TimeInterval? + + private var fileDescriptor: CInt = -1 + private var dispatchSource: DispatchSourceFileSystemObject? + + internal let url: URL + + init(for url: URL, eventMask: DispatchSource.FileSystemEvent, onChange: @escaping () -> Void) { + self.url = url + + fileDescriptor = open(url.path, O_EVTONLY) + + dispatchSource = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fileDescriptor, + eventMask: eventMask, + queue: self.queue + ) + + dispatchSource?.setEventHandler(handler: { + let distance = self.lastUpdate?.distance(to: Date().timeIntervalSince1970) + + if distance == nil || distance != nil && distance! > 1.00 { + // FS event fired, checking in 1s, no duplicate FS events will be acted upon + self.lastUpdate = Date().timeIntervalSince1970 + + Task { + await delay(seconds: 1) + onChange() + } + } + }) + + dispatchSource?.setCancelHandler(handler: { [weak self] in + guard let self = self else { return } + + close(self.fileDescriptor) + self.fileDescriptor = -1 + self.dispatchSource = nil + }) + + dispatchSource?.resume() + } + + func terminate() { + dispatchSource?.cancel() + } + + deinit { + Log.perf("FSNotifier for \(self.url) will be deinitialized.") + } +} diff --git a/phpmon/Common/Helpers/Measurements.swift b/phpmon/Common/Helpers/Measurements.swift new file mode 100644 index 0000000..d0ff231 --- /dev/null +++ b/phpmon/Common/Helpers/Measurements.swift @@ -0,0 +1,17 @@ +// +// Measurements.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 02/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +public struct Measurement { + let started = Date() + + var milliseconds: Double { + return round(Date().timeIntervalSince(started) * 1000 * 1000) / 1000 + } +} diff --git a/phpmon/Common/Helpers/PMWindowController.swift b/phpmon/Common/Helpers/PMWindowController.swift index e35ae7e..a8393d7 100644 --- a/phpmon/Common/Helpers/PMWindowController.swift +++ b/phpmon/Common/Helpers/PMWindowController.swift @@ -37,13 +37,13 @@ class PMWindowController: NSWindowController, NSWindowDelegate { extension NSWindowController { - public func positionWindowInTopLeftCorner() { + public func positionWindowInTopLeftCorner(offsetY: CGFloat = 0, offsetX: CGFloat = 0) { guard let frame = NSScreen.main?.frame else { return } guard let window = self.window else { return } window.setFrame(NSRect( - x: frame.size.width - window.frame.size.width - 20, - y: frame.size.height - window.frame.size.height - 40, + x: frame.size.width - window.frame.size.width - 20 + offsetX, + y: frame.size.height - window.frame.size.height - 40 + offsetY, width: window.frame.width, height: window.frame.height ), display: true) diff --git a/phpmon/Common/Helpers/System.swift b/phpmon/Common/Helpers/System.swift index d89b484..f3a1305 100644 --- a/phpmon/Common/Helpers/System.swift +++ b/phpmon/Common/Helpers/System.swift @@ -27,9 +27,20 @@ public func system(_ command: String) -> String { return output } -/** Same as the `system` command, but does not return the output. */ +/** + Same as the `system` command, but does not return the output. + */ public func system_quiet(_ command: String) { - _ = system(command) + let task = Process() + task.launchPath = "/bin/sh" + task.arguments = ["-c", command] + + let pipe = Pipe() + task.standardOutput = pipe + task.launch() + + _ = pipe.fileHandleForReading.readDataToEndOfFile() + return } /** diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index 97c363e..c4ea2ea 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -32,18 +32,25 @@ class ActivePhpInstallation { // MARK: - Computed var formula: String { - return (version.short == PhpEnv.brewPhpAlias) ? "php" : "php@\(version.short)" + return (version.short == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version.short)" } // MARK: - Initializer + public static func load() -> ActivePhpInstallation? { + if !FileSystem.fileExists(Paths.phpConfig) { + return nil + } + + return ActivePhpInstallation() + } + init() { // Show information about the current version do { try determineVersion() } catch { - // TODO: In future versions of PHP Monitor, this should not crash - fatalError("Could not determine or parse PHP version; aborting") + fatalError("Could not determine or parse PHP version; aborting!") } // Initialize the list of ini files that are loaded @@ -122,29 +129,19 @@ class ActivePhpInstallation { return "∞" } - // Check if the syntax is valid otherwise - let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) - let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first - return (match == nil) ? "⚠️" : "\(value)B" - } - - /** - Determine if PHP-FPM is configured correctly. - - For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent - versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, - that means that Valet won't work properly. - */ - func checkPhpFpmStatus() async -> Bool { - if self.version.short == "5.6" { - // The main PHP config file should contain `valet.sock` and then we're probably fine? - let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" - return await Shell.pipe("cat \(fileName)").out - .contains("valet.sock") + if value.isEmpty { + return "⚠️" } - // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) - return FileSystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf") + // Check if the syntax is valid otherwise + let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) + + let match = regex.matches( + in: value, options: [], + range: NSRange(location: 0, length: value.count) + ).first + + return (match == nil) ? "⚠️" : "\(value)B" } // MARK: - Structs diff --git a/phpmon/Common/PHP/Extensions/Xdebug.swift b/phpmon/Common/PHP/Extensions/Xdebug.swift index 615d71e..5f70d9d 100644 --- a/phpmon/Common/PHP/Extensions/Xdebug.swift +++ b/phpmon/Common/PHP/Extensions/Xdebug.swift @@ -12,11 +12,11 @@ import Cocoa class Xdebug { public static var enabled: Bool { - return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil + return PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") != nil } public static var activeModes: [String] { - guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else { return [] } diff --git a/phpmon/Common/PHP/Homebrew/HomebrewPackage.swift b/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift similarity index 66% rename from phpmon/Common/PHP/Homebrew/HomebrewPackage.swift rename to phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift index b250c80..23ef0bc 100644 --- a/phpmon/Common/PHP/Homebrew/HomebrewPackage.swift +++ b/phpmon/Common/PHP/Homebrew/HomebrewDecodable.swift @@ -1,5 +1,5 @@ // -// HomebrewPackage.swift +// HomebrewDecodable.swift // PHP Monitor // // Copyright © 2023 Nico Verbruggen. All rights reserved. @@ -17,7 +17,6 @@ struct HomebrewPackage: Decodable { return aliases.first! .replacingOccurrences(of: "php@", with: "") } - } struct HomebrewInstalled: Decodable { @@ -26,3 +25,15 @@ struct HomebrewInstalled: Decodable { let installed_as_dependency: Bool let installed_on_request: Bool } + +struct OutdatedFormulae: Decodable { + let formulae: [OutdatedFormula] +} + +struct OutdatedFormula: Decodable { + let name: String + let installed_versions: [String] + let current_version: String + let pinned: Bool + let pinned_version: String? +} diff --git a/phpmon/Common/PHP/PHP Version/PhpEnv.swift b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift similarity index 71% rename from phpmon/Common/PHP/PHP Version/PhpEnv.swift rename to phpmon/Common/PHP/PHP Version/PhpEnvironments.swift index 49dccaa..7d0a1f8 100644 --- a/phpmon/Common/PHP/PHP Version/PhpEnv.swift +++ b/phpmon/Common/PHP/PHP Version/PhpEnvironments.swift @@ -1,5 +1,5 @@ // -// PhpSwitcher.swift +// PhpEnvironments.swift // PHP Monitor // // Created by Nico Verbruggen on 21/12/2021. @@ -8,14 +8,27 @@ import Foundation -class PhpEnv { +class PhpEnvironments { // MARK: - Initializer + /** + + */ init() { - self.currentInstall = ActivePhpInstallation() + 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. + */ func determinePhpAlias() async { let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out @@ -32,11 +45,18 @@ class PhpEnv { /** The delegate that is informed of updates. */ weak var delegate: PhpSwitcherDelegate? - /** The static app instance. Accessible at any time. */ - static let shared = PhpEnv() + /** The static instance. Accessible at any time. */ + static let shared = PhpEnvironments() /** Whether the switcher is busy performing any actions. */ - var isBusy: Bool = false + var isBusy: Bool = false { + didSet { + Task { @MainActor in + MainMenu.shared.setBusyImage() + MainMenu.shared.rebuild() + } + } + } /** All versions of PHP that are currently supported. */ var availablePhpVersions: [String] = [] @@ -48,7 +68,7 @@ class PhpEnv { var cachedPhpInstallations: [String: PhpInstallation] = [:] /** Information about the currently linked PHP installation. */ - var currentInstall: ActivePhpInstallation! + var currentInstall: ActivePhpInstallation? /** The version that the `php` formula via Brew is aliased to on the current system. @@ -60,15 +80,15 @@ class PhpEnv { As such, we take that information from Homebrew. */ static var brewPhpAlias: String { - if Homebrew.fake { return "8.2" } + if PhpEnvironments.shared.homebrewPackage == nil { return "8.2" } - return Self.shared.homebrewPackage.version + return PhpEnvironments.shared.homebrewPackage.version } /** The currently linked and active PHP installation. */ - static var phpInstall: ActivePhpInstallation { + static var phpInstall: ActivePhpInstallation? { return Self.shared.currentInstall } @@ -79,25 +99,45 @@ class PhpEnv { // 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` of installations that are considered valid. */ public func detectPhpVersions() async -> Set { let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out let versions = await extractPhpVersions(from: files.components(separatedBy: "\n")) - let supportedByValet = Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? [] + let supportedByValet: Set = { + guard let version = Valet.shared.version else { + return Constants.DetectedPhpVersions + } - var supportedVersions = versions.intersection(supportedByValet) + 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` @@ -167,6 +207,10 @@ class PhpEnv { 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 @@ -179,7 +223,12 @@ class PhpEnv { Validates whether the currently running version matches the provided version. */ public func validate(_ version: String) -> Bool { - if self.currentInstall.version.short == version { + 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) @@ -195,7 +244,11 @@ class PhpEnv { You can then use the configuration file instance to change values. */ public func getConfigFile(forKey key: String) -> PhpConfigurationFile? { - return PhpEnv.phpInstall.iniFiles + guard let install = PhpEnvironments.phpInstall else { + return nil + } + + return install.iniFiles .reversed() .first(where: { $0.has(key: key) }) } diff --git a/phpmon/Common/PHP/PHP Version/PhpHelper.swift b/phpmon/Common/PHP/PHP Version/PhpHelper.swift index 75aa912..be41b47 100644 --- a/phpmon/Common/PHP/PHP Version/PhpHelper.swift +++ b/phpmon/Common/PHP/PHP Version/PhpHelper.swift @@ -28,10 +28,12 @@ class PhpHelper { Task { // Create the appropriate folders and check if the files exist do { if !FileSystem.directoryExists("~/.config/phpmon/bin") { - try FileSystem.createDirectory( - "~/.config/phpmon/bin", - withIntermediateDirectories: true - ) + Task { @MainActor in + try FileSystem.createDirectory( + "~/.config/phpmon/bin", + withIntermediateDirectories: true + ) + } } if FileSystem.fileExists(destination) { @@ -48,21 +50,14 @@ class PhpHelper { .resolvingSymlinksInPath().path // The contents of the script! - let script = """ - #!/bin/zsh - # \(keyPhrase) - # It reflects the location of PHP \(version)'s binaries on your system. - # Usage: . pm\(dotless) - [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\ - && echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\ - || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!"; - export PATH=\(path):$PATH - """ + let script = script(path, keyPhrase, version, dotless) - try FileSystem.writeAtomicallyToFile(destination, content: script) + Task { @MainActor in + try FileSystem.writeAtomicallyToFile(destination, content: script) - if !FileSystem.isExecutableFile(destination) { - try FileSystem.makeExecutable(destination) + if !FileSystem.isExecutableFile(destination) { + try FileSystem.makeExecutable(destination) + } } // Create a symlink if the folder is not in the PATH @@ -83,6 +78,24 @@ class PhpHelper { } } + private static func script( + _ path: String, + _ keyPhrase: String, + _ version: String, + _ dotless: String + ) -> String { + return """ + #!/bin/zsh + # \(keyPhrase) + # It reflects the location of PHP \(version)'s binaries on your system. + # Usage: . pm\(dotless) + [[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\ + && echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\ + || echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!"; + export PATH=\(path):$PATH + """ + } + private static func createSymlink(_ dotless: String) async { let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)" let destination = "/usr/local/bin/pm\(dotless)" diff --git a/phpmon/Common/PHP/PhpInstallation.swift b/phpmon/Common/PHP/PhpInstallation.swift index 4b19881..5658747 100644 --- a/phpmon/Common/PHP/PhpInstallation.swift +++ b/phpmon/Common/PHP/PhpInstallation.swift @@ -12,13 +12,17 @@ class PhpInstallation { var versionNumber: VersionNumber + var isHealthy: Bool = true + /** - In order to determine details about a PHP installation, we’ll simply run `php-config --version` - in the relevant directory. + In order to determine details about a PHP installation, + we’ll simply run `php-config --version` in the relevant directory. */ init(_ version: String) { - let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" + + let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php" + self.versionNumber = VersionNumber.make(from: version)! if FileSystem.fileExists(phpConfigExecutablePath) { @@ -32,6 +36,21 @@ class PhpInstallation { // If so, the app SHOULD crash, so that the users report what's up. self.versionNumber = try! VersionNumber.parse(longVersionString) } - } + if FileSystem.fileExists(phpExecutablePath) { + let testCommand = Command.execute( + path: phpExecutablePath, + arguments: ["-v"], + trimNewlines: false, + withStandardError: true + ).trimmingCharacters(in: .whitespacesAndNewlines) + + // If the "dyld: Library not loaded" issue pops up, we have an unhealthy PHP installation + // and we will need to reinstall this version of PHP via Homebrew. + if testCommand.contains("Library not loaded") && testCommand.contains("dyld") { + self.isHealthy = false + Log.err("The PHP installation of \(self.versionNumber.short) is not healthy!") + } + } + } } diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift new file mode 100644 index 0000000..83de877 --- /dev/null +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift @@ -0,0 +1,134 @@ +// +// InternalSwitcher+Valet.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 14/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +extension InternalSwitcher { + + typealias FixApplied = Bool + + public func ensureValetConfigurationIsValidForPhpVersion(_ version: String) async -> FixApplied { + // Early exit if Valet is not installed + if !Valet.installed { + assertionFailure("Cannot ensure that Valet configuration is valid if Valet is not installed.") + return false + } + + let corrections = [ + await self.disableDefaultPhpFpmPool(version), + await self.ensureConfigurationFilesExist(version) + ] + + return corrections.contains(true) + } + + // MARK: - PHP FPM pool + + public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied { + let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" + + if FileSystem.fileExists(pool) { + Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).") + let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" + let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon" + do { + if FileSystem.fileExists(new) { + Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), " + + "cleaning up so the newer `www.conf` can be moved again.") + try FileSystem.remove(new) + } + try FileSystem.move(from: existing, to: new) + Log.info("Success: A default `www.conf` file was disabled for PHP \(version).") + return true + } catch { + Log.err(error) + return false + } + } + + return false + } + + func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] { + return [ + ExpectedConfigurationFile( + destination: "/php-fpm.d/valet-fpm.conf", + source: "/cli/stubs/etc-phpfpm-valet.conf", + replacements: [ + "VALET_USER": Paths.whoami, + "VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory, + "valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock" + ], + applies: { Valet.shared.version!.major > 2 } + ), + ExpectedConfigurationFile( + destination: "/conf.d/error_log.ini", + source: "/cli/stubs/etc-phpfpm-error_log.ini", + replacements: [ + "VALET_USER": Paths.whoami, + "VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory + ], + applies: { return true } + ), + ExpectedConfigurationFile( + destination: "/conf.d/php-memory-limits.ini", + source: "/cli/stubs/php-memory-limits.ini", + replacements: [:], + applies: { return true } + ) + ] + } + + func ensureConfigurationFilesExist(_ version: String) async -> FixApplied { + let files = self.getExpectedConfigurationFiles(for: version) + + // For each of the files, attempt to fix anything that is wrong + let outcomes = files.map { file in + let configFileExists = FileSystem.fileExists("\(Paths.etcPath)/php/\(version)/" + file.destination) + + if configFileExists { + return false + } + + Log.info("Config file `\(file.destination)` does not exist, will attempt to automatically fix!") + + if !file.applies() { + return false + } + + do { + var contents = try FileSystem.getStringFromFile("~/.composer/vendor/laravel/valet" + file.source) + + for (original, replacement) in file.replacements { + contents = contents.replacingOccurrences(of: original, with: replacement) + } + + try FileSystem.writeAtomicallyToFile( + "\(Paths.etcPath)/php/\(version)" + file.destination, + content: contents + ) + } catch { + Log.err("Automatically fixing \(file.destination) did not work.") + return false + } + + return true + } + + // If any fixes were applied, return true + return outcomes.contains(true) + } + +} + +public struct ExpectedConfigurationFile { + let destination: String + let source: String + let replacements: [String: String] + let applies: () -> Bool +} diff --git a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift index 5b465aa..ca5087d 100644 --- a/phpmon/Common/PHP/Switcher/InternalSwitcher.swift +++ b/phpmon/Common/PHP/Switcher/InternalSwitcher.swift @@ -25,10 +25,9 @@ class InternalSwitcher: PhpSwitcher { let versions = getVersionsToBeHandled(version) await withTaskGroup(of: String.self, body: { group in - for available in PhpEnv.shared.availablePhpVersions { + for available in PhpEnvironments.shared.availablePhpVersions { group.addTask { - await self.disableDefaultPhpFpmPool(available) - await self.stopPhpVersion(available) + await self.unlinkAndStopPhpVersion(available) return available } } @@ -42,12 +41,19 @@ class InternalSwitcher: PhpSwitcher { Log.info("Linking the new version \(version)!") for formula in versions { + if Valet.installed { + Log.info("Ensuring that the Valet configuration is valid...") + _ = await self.ensureValetConfigurationIsValidForPhpVersion(formula) + } + Log.info("Will start PHP \(version)... (primary: \(version == formula))") - await self.startPhpVersion(formula, primary: (version == formula)) + await self.linkAndStartPhpVersion(formula, primary: (version == formula)) } - Log.info("Restarting nginx, just to be sure!") - await brew("services restart nginx", sudo: true) + if Valet.installed { + Log.info("Restarting nginx, just to be sure!") + await brew("services restart nginx", sudo: true) + } Log.info("The new version(s) have been linked!") }) @@ -69,56 +75,36 @@ class InternalSwitcher: PhpSwitcher { return versions } - func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool { - let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" - return FileSystem.fileExists(pool) + func unlinkAndStopPhpVersion(_ version: String) async { + let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)" + await brew("unlink \(formula)") + + if Valet.installed { + await brew("services stop \(formula)", sudo: true) + Log.info("Unlinked and stopped services for \(formula)") + } else { + Log.info("Unlinked \(formula)") + } } - func disableDefaultPhpFpmPool(_ version: String) async { - let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" - if FileSystem.fileExists(pool) { - Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).") - let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf" - let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon" - do { - if FileSystem.fileExists(new) { - Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), " - + "cleaning up so the newer `www.conf` can be moved again.") - try FileSystem.remove(new) - } - try FileSystem.move(from: existing, to: new) - Log.info("Success: A default `www.conf` file was disabled for PHP \(version).") - } catch { - Log.err(error) + func linkAndStartPhpVersion(_ version: String, primary: Bool) async { + let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)" + + if primary { + Log.info("\(formula) is the primary formula, linking...") + await brew("link \(formula) --overwrite --force") + } else { + Log.info("\(formula) is an isolated PHP version, not linking!") + } + + if Valet.installed { + await brew("services start \(formula)", sudo: true) + + if Valet.enabled(feature: .isolatedSites) && primary { + let socketVersion = version.replacingOccurrences(of: ".", with: "") + await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock") + Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).") } } } - - func stopPhpVersion(_ version: String) async { - let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)" - await brew("unlink \(formula)") - await brew("services stop \(formula)", sudo: true) - Log.info("Unlinked and stopped services for \(formula)") - } - - func startPhpVersion(_ version: String, primary: Bool) async { - let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)" - - if primary { - Log.info("\(formula) is the primary formula, linking and starting services...") - await brew("link \(formula) --overwrite --force") - } else { - Log.info("\(formula) is an isolated PHP version, starting services only...") - } - - await brew("services start \(formula)", sudo: true) - - if Valet.enabled(feature: .isolatedSites) && primary { - let socketVersion = version.replacingOccurrences(of: ".", with: "") - await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock") - Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).") - } - - } - } diff --git a/phpmon/Common/Testables/TestableCommand.swift b/phpmon/Common/Testables/TestableCommand.swift index b7e8503..a3679db 100644 --- a/phpmon/Common/Testables/TestableCommand.swift +++ b/phpmon/Common/Testables/TestableCommand.swift @@ -19,6 +19,10 @@ class TestableCommand: CommandProtocol { self.execute(path: path, arguments: arguments, trimNewlines: false) } + public func execute(path: String, arguments: [String], trimNewlines: Bool, withStandardError: Bool) -> String { + self.execute(path: path, arguments: arguments, trimNewlines: trimNewlines) + } + public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String { let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))" assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found") diff --git a/phpmon/Common/Testables/TestableConfiguration.swift b/phpmon/Common/Testables/TestableConfiguration.swift index 2dbb33e..bdcb206 100644 --- a/phpmon/Common/Testables/TestableConfiguration.swift +++ b/phpmon/Common/Testables/TestableConfiguration.swift @@ -15,10 +15,93 @@ public struct TestableConfiguration: Codable { var commandOutput: [String: String] var preferenceOverrides: [PreferenceName: Bool] + init( + architecture: String, + filesystem: [String: FakeFile], + shellOutput: [String: BatchFakeShellOutput], + commandOutput: [String: String], + preferenceOverrides: [PreferenceName: Bool], + phpVersions: [VersionNumber] + ) { + self.architecture = architecture + self.filesystem = filesystem + self.shellOutput = shellOutput + self.commandOutput = commandOutput + self.preferenceOverrides = preferenceOverrides + + phpVersions.enumerated().forEach { (index, version) in + self.addPhpVersion(version, primary: index == 0) + } + } + + private enum CodingKeys: String, CodingKey { + case architecture, filesystem, shellOutput, commandOutput, preferenceOverrides + } + + // MARK: Add PHP versions + + private var primaryPhpVersion: VersionNumber? + private var secondaryPhpVersions: [VersionNumber] = [] + + mutating func addPhpVersion(_ version: VersionNumber, primary: Bool) { + if primary { + if primaryPhpVersion != nil { + fatalError("You cannot add multiple primary PHP versions to a testable configuration!") + } + primaryPhpVersion = version + } else { + self.secondaryPhpVersions.append(version) + } + + self.filesystem = self.filesystem.merging([ + "/opt/homebrew/opt/php@\(version.short)/bin/php" + : .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php"), + "/opt/homebrew/Cellar/php/\(version.long)/bin/php" + : .fake(.binary), + "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config" + : .fake(.binary), + "/opt/homebrew/etc/php/\(version.short)/php-fpm.d/www.conf" + : .fake(.text), + "/opt/homebrew/etc/php/\(version.short)/php-fpm.d/valet-fpm.conf" + : .fake(.text), + "/opt/homebrew/etc/php/\(version.short)/php.ini" + : .fake(.text), + "/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini" + : .fake(.text) + ]) { (_, new) in new } + + if primary { + self.shellOutput["ls /opt/homebrew/opt | grep php"] + = .instant("php") + self.filesystem["/opt/homebrew/opt/php"] + = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)") + self.filesystem["/opt/homebrew/opt/php/bin/php"] + = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php") + self.filesystem["/opt/homebrew/bin/php"] + = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php") + self.filesystem["/opt/homebrew/bin/php-config"] + = .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)/bin/php-config") + self.commandOutput["/opt/homebrew/bin/php-config --version"] + = version.long + self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] = + """ + /opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini, + """ + } else { + self.shellOutput["ls /opt/homebrew/opt | grep php@"] = + BatchFakeShellOutput.instant( + self.secondaryPhpVersions + .map { "php@\($0.short)" } + .joined(separator: "\n") + ) + } + } + + // MARK: Interactions + func apply() { Log.separator() Log.info("USING TESTABLE CONFIGURATION...") - Homebrew.fake = true Log.separator() Log.info("Applying fake shell...") ActiveShell.useTestable(shellOutput) @@ -26,18 +109,23 @@ public struct TestableConfiguration: Codable { ActiveFileSystem.useTestable(filesystem) Log.info("Applying fake commands...") ActiveCommand.useTestable(commandOutput) - Log.info("Applying fake scanner...") - ValetScanner.useFake() - Log.info("Applying fake services manager...") - ServicesManager.useFake() - Log.info("Applying fake Valet domain interactor...") - ValetInteractor.useFake() Log.info("Applying temporary preference overrides...") preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in Preferences.shared.cachedPreferences[key] = value } + + if Valet.shared.installed { + Log.info("Applying fake scanner...") + ValetScanner.useFake() + Log.info("Applying fake services manager...") + ServicesManager.useFake() + Log.info("Applying fake Valet domain interactor...") + ValetInteractor.useFake() + } } + // MARK: Persist and load + func toJson(pretty: Bool = false) -> String { let data = try! JSONEncoder().encode(self) diff --git a/phpmon/Credits.html b/phpmon/Credits.html index 078adb3..4f45299 100644 --- a/phpmon/Credits.html +++ b/phpmon/Credits.html @@ -13,11 +13,13 @@
-

Do you enjoy using the app? Leave a star on GitHub!

+

Do you enjoy using the app? Is it helping you save time? Leave a star on GitHub!

Having issues? Consult the FAQ section, I did my best to ensure everything is documented.

Want to support further development of PHP Monitor? You can financially support the continued development of this app.

-

Get the latest on Mastodon. Give me a follow on Mastodon to learn about what's brewing and when new updates drop.

-
+

Get the latest on Twitter or Mastodon. Give me a follow on Twitter or Mastodon to learn about what's brewing and when new updates drop.

+

Special thanks to all current and past sponsors of PHP Monitor, who have helped to make further development of the app possible.

+

Made possible by these GitHub Sponsors: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.
(Some names have been omitted due to their sponsorships being private. Thank you all!) +
diff --git a/phpmon/Domain/App/App.swift b/phpmon/Domain/App/App.swift index bfb5659..a73922a 100644 --- a/phpmon/Domain/App/App.swift +++ b/phpmon/Domain/App/App.swift @@ -62,9 +62,6 @@ class App { // MARK: Variables - /** Technical information about the current environment. */ - var environment = EnvironmentManager() - /** The list of preferences that are currently active. */ var preferences: [PreferenceName: Bool]! @@ -80,12 +77,18 @@ class App { /** The window controller of the warnings window. */ var warningsWindowController: WarningsWindowController? + /** The window controller of the warnings window. */ + var versionManagerWindowController: PhpVersionManagerWindowController? + /** List of detected (installed) applications that PHP Monitor can work with. */ var detectedApplications: [Application] = [] /** The warning manager, responsible for keeping track of warnings. */ var warnings = WarningManager.shared + /** The filesystem watchers, responsible for keeping track of changes to the PHP installation. */ + var watchers: [FSNotifier.Kind: FSNotifier] = [:] + /** Timer that will periodically reload info about the user's PHP installation. */ var timer: Timer? diff --git a/phpmon/Domain/App/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift index 37be68d..ca73add 100644 --- a/phpmon/Domain/App/AppDelegate.swift +++ b/phpmon/Domain/App/AppDelegate.swift @@ -11,6 +11,10 @@ import UserNotifications @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { + static var instance: AppDelegate { + return NSApplication.shared.delegate as! AppDelegate + } + // MARK: - Variables /** @@ -38,11 +42,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele let valet: Valet /** - The PhpEnv singleton that handles PHP version + The Brew singleton that contains all information about Homebrew + and its configuration on your system. + */ + let brew: Brew + + /** + The PhpEnvironments singleton that handles PHP version detection, as well as switching. It is initialized when the app is ready and passed all checks. */ - var phpEnvironment: PhpEnv! = nil + var phpEnvironments: PhpEnvironments! = nil /** The logger is responsible for different levels of logging. @@ -58,7 +68,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele override init() { #if DEBUG logger.verbosity = .performance - if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) { Self.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: "")) } @@ -88,11 +97,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele self.menu = MainMenu.shared self.paths = Paths.shared self.valet = Valet.shared + self.brew = Brew.shared super.init() } func initializeSwitcher() { - self.phpEnvironment = PhpEnv.shared + self.phpEnvironments = PhpEnvironments.shared } static func initializeTestingProfile(_ path: String) { @@ -110,9 +120,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele func applicationDidFinishLaunching(_ aNotification: Notification) { // Make sure notifications will work setupNotifications() + Task { // Make sure the menu performs its initial checks await menu.startup() } } + // MARK: - Menu Items + + @IBOutlet weak var menuItemSites: NSMenuItem! + + /** + Ensure relevant menu items in the main menu bar (not the pop-up menu) + are disabled or hidden when needed. + */ + public func configureMenuItems(standalone: Bool) { + if standalone { + menuItemSites.isHidden = true + } + } } diff --git a/phpmon/Domain/App/AppUpdater.swift b/phpmon/Domain/App/AppUpdater.swift index 26b9d7c..a2a985c 100644 --- a/phpmon/Domain/App/AppUpdater.swift +++ b/phpmon/Domain/App/AppUpdater.swift @@ -24,9 +24,13 @@ class AppUpdater { Log.info("The app will search for updates...") - let caskUrl = App.identifier.contains(".dev") - ? Constants.Urls.DevBuildCaskFile - : Constants.Urls.StableBuildCaskFile + var caskUrl = Constants.Urls.StableBuildCaskFile + + if App.identifier.contains(".phpmon.eap") { + caskUrl = Constants.Urls.EarlyAccessCaskFile + } else if App.identifier.contains(".phpmon.dev") { + caskUrl = Constants.Urls.DevBuildCaskFile + } guard let caskFile = await CaskFile.from(url: caskUrl) else { Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.") @@ -73,7 +77,7 @@ class AppUpdater { .localized(latestVersionOnline.humanReadable), subtitle: "updater.alerts.newer_version_available.subtitle" .localized, - description: HomebrewDiagnostics.customCaskInstalled + description: BrewDiagnostics.customCaskInstalled ? "updater.installation_source.brew".localized(command) : "updater.installation_source.direct".localized ) @@ -88,11 +92,15 @@ class AppUpdater { .withSecondary( text: "updater.alerts.buttons.release_notes".localized, action: { _ in - let urlSegments = self.caskFile.url.split(separator: "/") - let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip} - NSWorkspace.shared.open( - Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)") - ) + NSWorkspace.shared.open({ + if App.identifier.contains(".eap") { + return Constants.Urls.EarlyAccessChangelog + } else { + let urlSegments = self.caskFile.url.split(separator: "/") + let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip} + return Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)") + } + }()) } ) .withTertiary(text: "updater.alerts.buttons.dismiss".localized, action: { vc in @@ -179,11 +187,19 @@ class AppUpdater { // Cleanup the upgrade.success file if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") { Task { @MainActor in - LocalNotification.send( - title: "notification.phpmon_updated.title".localized, - subtitle: "notification.phpmon_updated.desc".localized(App.shortVersion), - preference: nil - ) + if App.identifier.contains(".phpmon.eap") || App.identifier.contains(".phpmon.dev") { + LocalNotification.send( + title: "notification.phpmon_updated.title".localized, + subtitle: "notification.phpmon_updated_dev.desc".localized(App.shortVersion, App.bundleVersion), + preference: nil + ) + } else { + LocalNotification.send( + title: "notification.phpmon_updated.title".localized, + subtitle: "notification.phpmon_updated.desc".localized(App.shortVersion), + preference: nil + ) + } } Log.info("The `upgrade.success` file was found! An update was installed. Cleaning up...") diff --git a/phpmon/Domain/App/Base.lproj/Main.storyboard b/phpmon/Domain/App/Base.lproj/Main.storyboard index d7aa2aa..a2302ba 100644 --- a/phpmon/Domain/App/Base.lproj/Main.storyboard +++ b/phpmon/Domain/App/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -34,18 +34,6 @@ - - -

- - - - - - - - - @@ -82,12 +70,12 @@ - + - + @@ -297,6 +285,18 @@ + + + + + + + + + + + + @@ -317,7 +317,11 @@ - + + + + + diff --git a/phpmon/Domain/App/EnvironmentCheck.swift b/phpmon/Domain/App/EnvironmentCheck.swift index cb9bf5a..e3b4b2b 100644 --- a/phpmon/Domain/App/EnvironmentCheck.swift +++ b/phpmon/Domain/App/EnvironmentCheck.swift @@ -43,3 +43,9 @@ struct EnvironmentCheck { return await !self.command() } } + +struct EnvironmentCheckGroup { + let name: String + let condition: () -> Bool + let checks: [EnvironmentCheck] +} diff --git a/phpmon/Domain/App/EnvironmentManager.swift b/phpmon/Domain/App/EnvironmentManager.swift deleted file mode 100644 index fa527d6..0000000 --- a/phpmon/Domain/App/EnvironmentManager.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// EnvironmentManager.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 14/09/2022. -// Copyright © 2023 Nico Verbruggen. All rights reserved. -// - -import Foundation - -public class EnvironmentManager { - var values: [EnvironmentProperty: Bool] = [:] - - public func process() async { - self.values[.hasValetInstalled] = await !{ - let output = await Shell.pipe("valet --version").out - - // Failure condition #1: does not contain Laravel Valet - if !output.contains("Laravel Valet") { - return false - } - - // Extract the version number - Valet.shared.version = try! VersionNumber.parse(VersionExtractor.from(output)!) - - // Get the actual version - return Valet.shared.version == nil - }() // returns true if none of the failure conditions are met - } -} - -public enum EnvironmentProperty { - case hasHomebrewInstalled - case hasValetInstalled -} diff --git a/phpmon/Domain/App/Services/ServicesManager.swift b/phpmon/Domain/App/Services/ServicesManager.swift index d2d51f4..fb8017f 100644 --- a/phpmon/Domain/App/Services/ServicesManager.swift +++ b/phpmon/Domain/App/Services/ServicesManager.swift @@ -107,9 +107,9 @@ class ServicesManager: ObservableObject { var formulae: [HomebrewFormula] { var formulae = [ - Homebrew.Formulae.php, - Homebrew.Formulae.nginx, - Homebrew.Formulae.dnsmasq + HomebrewFormulae.php, + HomebrewFormulae.nginx, + HomebrewFormulae.dnsmasq ] let additionalFormulae = (Preferences.custom.services ?? []).map({ item in diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index f9e7832..cea3bbf 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -19,24 +19,32 @@ class Startup { */ func checkEnvironment() async -> Bool { // Do the important system setup checks - Log.info("[ARCH] The user is running PHP Monitor with the architecture: \(App.architecture)") + Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)") - for check in self.checks { - if await check.succeeds() { - Log.info("[OK] \(check.name)") - continue + for group in self.groups { + if group.condition() { + Log.info("Now running \(group.checks.count) \(group.name) checks!") + for check in group.checks { + let start = Measurement() + if await check.succeeds() { + Log.info("[PASS] \(check.name) (\(start.milliseconds) ms)") + continue + } + + // If we get here, something's gone wrong and the check has failed... + Log.info("[FAIL] \(check.name) (\(start.milliseconds) ms)") + await showAlert(for: check) + return false + } + } else { + Log.info("Skipping \(group.name) checks!") } - - // If we get here, something's gone wrong and the check has failed... - Log.info("[FAIL] \(check.name)") - await showAlert(for: check) - return false } // If we get here, nothing has gone wrong. That's what we want! initializeSwitcher() - Log.separator(as: .info) Log.info("PHP Monitor has determined the application has successfully passed all checks.") + Log.separator(as: .info) return true } @@ -81,188 +89,208 @@ class Startup { // MARK: - Check (List) - public var checks: [EnvironmentCheck] = [ - // ================================================================================= - // The Homebrew binary must exist. - // ================================================================================= - EnvironmentCheck( - command: { return !FileSystem.fileExists(Paths.brew) }, - name: "`\(Paths.brew)` exists", - titleText: "alert.homebrew_missing.title".localized, - subtitleText: "alert.homebrew_missing.subtitle".localized, - descriptionText: "alert.homebrew_missing.info".localized( - App.architecture - .replacingOccurrences(of: "x86_64", with: "Intel") - .replacingOccurrences(of: "arm64", with: "Apple Silicon"), - Paths.brew + public var groups: [EnvironmentCheckGroup] = [ + EnvironmentCheckGroup(name: "core", condition: { return true }, checks: [ + // ================================================================================= + // The Homebrew binary must exist. + // ================================================================================= + EnvironmentCheck( + command: { return !FileSystem.fileExists(Paths.brew) }, + name: "`\(Paths.brew)` exists", + titleText: "alert.homebrew_missing.title".localized, + subtitleText: "alert.homebrew_missing.subtitle".localized, + descriptionText: "alert.homebrew_missing.info".localized( + App.architecture + .replacingOccurrences(of: "x86_64", with: "Intel") + .replacingOccurrences(of: "arm64", with: "Apple Silicon"), + Paths.brew + ), + buttonText: "alert.homebrew_missing.quit".localized, + requiresAppRestart: true ), - buttonText: "alert.homebrew_missing.quit".localized, - requiresAppRestart: true - ), - // ================================================================================= - // The PHP binary must exist. - // ================================================================================= - EnvironmentCheck( - command: { return !FileSystem.fileExists(Paths.php) }, - name: "`\(Paths.php)` exists", - titleText: "startup.errors.php_binary.title".localized, - subtitleText: "startup.errors.php_binary.subtitle".localized, - descriptionText: "startup.errors.php_binary.desc".localized(Paths.php) - ), - // ================================================================================= - // Make sure we can detect one or more PHP installations. - // ================================================================================= - EnvironmentCheck( - command: { - return await !Shell.pipe("ls \(Paths.optPath) | grep php").out.contains("php") - }, - name: "`ls \(Paths.optPath) | grep php` returned php result", - titleText: "startup.errors.php_opt.title".localized, - subtitleText: "startup.errors.php_opt.subtitle".localized( - Paths.optPath - ), - descriptionText: "startup.errors.php_opt.desc".localized - ), - // ================================================================================= - // The Valet binary must exist. - // ================================================================================= - EnvironmentCheck( - command: { - return !(FileSystem.fileExists(Paths.valet) || FileSystem.fileExists("~/.composer/vendor/bin/valet")) - }, - name: "`valet` binary exists", - titleText: "startup.errors.valet_executable.title".localized, - subtitleText: "startup.errors.valet_executable.subtitle".localized, - descriptionText: "startup.errors.valet_executable.desc".localized( - Paths.valet + // ================================================================================= + // Make sure we can detect one or more PHP installations. + // ================================================================================= + EnvironmentCheck( + command: { + return await !Shell.pipe("ls \(Paths.optPath) | grep php").out.contains("php") + }, + name: "`ls \(Paths.optPath) | grep php` returned php result", + titleText: "startup.errors.php_opt.title".localized, + subtitleText: "startup.errors.php_opt.subtitle".localized( + Paths.optPath + ), + descriptionText: "startup.errors.php_opt.desc".localized ) - ), - // ================================================================================= - // Check if Valet and Homebrew need manual password intervention. If they do, then - // PHP Monitor will be unable to run these commands, which prevents PHP Monitor from - // functioning correctly. Let the user know that they need to run `valet trust`. - // ================================================================================= - EnvironmentCheck( - command: { return await !Shell.pipe("cat /private/etc/sudoers.d/brew").out.contains(Paths.brew) }, - name: "`/private/etc/sudoers.d/brew` contains brew", - titleText: "startup.errors.sudoers_brew.title".localized, - subtitleText: "startup.errors.sudoers_brew.subtitle".localized, - descriptionText: "startup.errors.sudoers_brew.desc".localized - ), - EnvironmentCheck( - command: { return await !Shell.pipe("cat /private/etc/sudoers.d/valet").out.contains(Paths.valet) }, - name: "`/private/etc/sudoers.d/valet` contains valet", - titleText: "startup.errors.sudoers_valet.title".localized, - subtitleText: "startup.errors.sudoers_valet.subtitle".localized, - descriptionText: "startup.errors.sudoers_valet.desc".localized - ), - // ================================================================================= - // Verify if the Homebrew services are running (as root). - // ================================================================================= - EnvironmentCheck( - command: { - await HomebrewDiagnostics.loadInstalledTaps() - return await HomebrewDiagnostics.cannotLoadService("dnsmasq") - }, - name: "`sudo \(Paths.brew) services info` JSON loaded", - titleText: "startup.errors.services_json_error.title".localized, - subtitleText: "startup.errors.services_json_error.subtitle".localized, - descriptionText: "startup.errors.services_json_error.desc".localized - ), - // ================================================================================= - // Determine that Valet is installed - // ================================================================================= - EnvironmentCheck( - command: { - return !FileSystem.directoryExists("~/.config/valet") - }, - name: "`.config/valet` not empty (Valet installed)", - titleText: "startup.errors.valet_not_installed.title".localized, - subtitleText: "startup.errors.valet_not_installed.subtitle".localized, - descriptionText: "startup.errors.valet_not_installed.desc".localized - ), - // ================================================================================= - // Determine that the Valet configuration JSON file is valid. - // ================================================================================= - EnvironmentCheck( - command: { - // Detect additional binaries (e.g. Composer) - Paths.shared.detectBinaryPaths() - // Load the configuration file (config.json) - Valet.shared.loadConfiguration() - // This check fails when the config is nil - return Valet.shared.config == nil - }, - name: "`config.json` was valid", - titleText: "startup.errors.valet_json_invalid.title".localized, - subtitleText: "startup.errors.valet_json_invalid.subtitle".localized, - descriptionText: "startup.errors.valet_json_invalid.desc".localized - ), - // ================================================================================= - // Check for `which` alias issue - // ================================================================================= - EnvironmentCheck( - command: { - let nodePath = await Shell.pipe("which node").out - return App.architecture == "x86_64" + ]), + EnvironmentCheckGroup(name: "valet", condition: { return Valet.installed }, checks: [ + // ================================================================================= + // The PHP binary must exist. + // ================================================================================= + EnvironmentCheck( + command: { return !FileSystem.fileExists(Paths.php) }, + name: "`\(Paths.php)` exists", + titleText: "startup.errors.php_binary.title".localized, + subtitleText: "startup.errors.php_binary.subtitle".localized, + descriptionText: "startup.errors.php_binary.desc".localized(Paths.php) + ), + // ================================================================================= + // Ensure that the main PHP installation is not broken. + // ================================================================================= + EnvironmentCheck( + command: { + return await Shell.pipe("\(Paths.binPath)/php -v").err + .contains("Library not loaded") + }, + name: "`no dyld issue detected", + titleText: "startup.errors.dyld_library.title".localized, + subtitleText: "startup.errors.dyld_library.subtitle".localized( + Paths.optPath + ), + descriptionText: "startup.errors.dyld_library.desc".localized + ), + // ================================================================================= + // The Valet binary must exist. + // ================================================================================= + EnvironmentCheck( + command: { + return !(FileSystem.fileExists(Paths.valet) + || FileSystem.fileExists("~/.composer/vendor/bin/valet")) + }, + name: "`valet` binary exists", + titleText: "startup.errors.valet_executable.title".localized, + subtitleText: "startup.errors.valet_executable.subtitle".localized, + descriptionText: "startup.errors.valet_executable.desc".localized( + Paths.valet + ) + ), + // ================================================================================= + // Check if Valet and Homebrew need manual password intervention. If they do, then + // PHP Monitor will be unable to run these commands, which prevents PHP Monitor from + // functioning correctly. Let the user know that they need to run `valet trust`. + // ================================================================================= + EnvironmentCheck( + command: { return await !Shell.pipe("cat /private/etc/sudoers.d/brew").out.contains(Paths.brew) }, + name: "`/private/etc/sudoers.d/brew` contains brew", + titleText: "startup.errors.sudoers_brew.title".localized, + subtitleText: "startup.errors.sudoers_brew.subtitle".localized, + descriptionText: "startup.errors.sudoers_brew.desc".localized + ), + EnvironmentCheck( + command: { return await !Shell.pipe("cat /private/etc/sudoers.d/valet").out.contains(Paths.valet) }, + name: "`/private/etc/sudoers.d/valet` contains valet", + titleText: "startup.errors.sudoers_valet.title".localized, + subtitleText: "startup.errors.sudoers_valet.subtitle".localized, + descriptionText: "startup.errors.sudoers_valet.desc".localized + ), + // ================================================================================= + // Determine that Valet is installed + // ================================================================================= + EnvironmentCheck( + command: { + return !FileSystem.directoryExists("~/.config/valet") + }, + name: "`.config/valet` not empty (Valet installed)", + titleText: "startup.errors.valet_not_installed.title".localized, + subtitleText: "startup.errors.valet_not_installed.subtitle".localized, + descriptionText: "startup.errors.valet_not_installed.desc".localized + ), + // ================================================================================= + // Determine that the Valet configuration JSON file is valid. + // ================================================================================= + EnvironmentCheck( + command: { + // Detect additional binaries (e.g. Composer) + Paths.shared.detectBinaryPaths() + // Load the configuration file (config.json) + Valet.shared.loadConfiguration() + // This check fails when the config is nil + return Valet.shared.config == nil + }, + name: "`config.json` was valid", + titleText: "startup.errors.valet_json_invalid.title".localized, + subtitleText: "startup.errors.valet_json_invalid.subtitle".localized, + descriptionText: "startup.errors.valet_json_invalid.desc".localized + ), + // ================================================================================= + // Verify if the Homebrew services are running (as root). + // ================================================================================= + EnvironmentCheck( + command: { + await BrewDiagnostics.loadInstalledTaps() + return await BrewDiagnostics.cannotLoadService("dnsmasq") + }, + name: "`sudo \(Paths.brew) services info` JSON loaded", + titleText: "startup.errors.services_json_error.title".localized, + subtitleText: "startup.errors.services_json_error.subtitle".localized, + descriptionText: "startup.errors.services_json_error.desc".localized + ), + // ================================================================================= + // Check for `which` alias issue + // ================================================================================= + EnvironmentCheck( + command: { + let nodePath = await Shell.pipe("which node").out + return App.architecture == "x86_64" && FileSystem.fileExists("/usr/local/bin/which") && nodePath.contains("env: node: No such file or directory") - }, - name: "`env: node` issue does not apply", - titleText: "startup.errors.which_alias_issue.title".localized, - subtitleText: "startup.errors.which_alias_issue.subtitle".localized, - descriptionText: "startup.errors.which_alias_issue.desc".localized - ), - // ================================================================================= - // Determine that Valet works correctly (no issues in platform detected) - // ================================================================================= - EnvironmentCheck( - command: { - return await Shell.pipe("valet --version").out - .contains("Composer detected issues in your platform") - }, - name: "`no global composer issues", - titleText: "startup.errors.global_composer_platform_issues.title".localized, - subtitleText: "startup.errors.global_composer_platform_issues.subtitle".localized, - descriptionText: "startup.errors.global_composer_platform_issues.desc".localized - ), - // ================================================================================= - // Determine the Valet version and ensure it isn't unknown. - // ================================================================================= - EnvironmentCheck( - command: { - let output = await Shell.pipe("valet --version").out - // Failure condition #1: does not contain Laravel Valet - if !output.contains("Laravel Valet") { - return true - } - // Failure condition #2: version cannot be parsed - let versionString = output - .trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: "Laravel Valet")[1] - .trimmingCharacters(in: .whitespaces) - // Extract the version number - Valet.shared.version = try! VersionNumber.parse(VersionExtractor.from(versionString)!) - // Get the actual version - return Valet.shared.version == nil - }, - name: "`valet --version` was loaded", - titleText: "startup.errors.valet_version_unknown.title".localized, - subtitleText: "startup.errors.valet_version_unknown.subtitle".localized, - descriptionText: "startup.errors.valet_version_unknown.desc".localized - ), - // ================================================================================= - // Ensure the Valet version is supported. - // ================================================================================= - EnvironmentCheck( - command: { - // We currently support Valet 2, 3 or 4. Any other version should get an alert. - return ![2, 3, 4].contains(Valet.shared.version.major) - }, - name: "valet version is supported", - titleText: "startup.errors.valet_version_not_supported.title".localized, - subtitleText: "startup.errors.valet_version_not_supported.subtitle".localized, - descriptionText: "startup.errors.valet_version_not_supported.desc".localized - ) + }, + name: "`env: node` issue does not apply", + titleText: "startup.errors.which_alias_issue.title".localized, + subtitleText: "startup.errors.which_alias_issue.subtitle".localized, + descriptionText: "startup.errors.which_alias_issue.desc".localized + ), + // ================================================================================= + // Determine that Valet works correctly (no issues in platform detected) + // ================================================================================= + EnvironmentCheck( + command: { + return await Shell.pipe("valet --version").out + .contains("Composer detected issues in your platform") + }, + name: "no global composer issues", + titleText: "startup.errors.global_composer_platform_issues.title".localized, + subtitleText: "startup.errors.global_composer_platform_issues.subtitle".localized, + descriptionText: "startup.errors.global_composer_platform_issues.desc".localized + ), + // ================================================================================= + // Determine the Valet version and ensure it isn't unknown. + // ================================================================================= + EnvironmentCheck( + command: { + let output = await Shell.pipe("valet --version").out + // Failure condition #1: does not contain Laravel Valet + if !output.contains("Laravel Valet") { + return true + } + // Failure condition #2: version cannot be parsed + let versionString = output + .trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: "Laravel Valet")[1] + .trimmingCharacters(in: .whitespaces) + // Extract the version number + Valet.shared.version = try! VersionNumber.parse(VersionExtractor.from(versionString)!) + // Get the actual version + return Valet.shared.version == nil + }, + name: "`valet --version` was loaded", + titleText: "startup.errors.valet_version_unknown.title".localized, + subtitleText: "startup.errors.valet_version_unknown.subtitle".localized, + descriptionText: "startup.errors.valet_version_unknown.desc".localized + ), + // ================================================================================= + // Ensure the Valet version is supported. + // ================================================================================= + EnvironmentCheck( + command: { + // We currently support Valet 2, 3 or 4. Any other version should get an alert. + return ![2, 3, 4].contains(Valet.shared.version?.major) + }, + name: "valet version is supported", + titleText: "startup.errors.valet_version_not_supported.title".localized, + subtitleText: "startup.errors.valet_version_not_supported.subtitle".localized, + descriptionText: "startup.errors.valet_version_not_supported.desc".localized + ) + ]) ] } diff --git a/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift b/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift index 9d2d73c..a0cbcf7 100644 --- a/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift +++ b/phpmon/Domain/DomainList/Cells/DomainListPhpCell.swift @@ -59,8 +59,12 @@ class DomainListPhpCell: NSTableCellView, DomainListCellProtocol { return [] } - return PhpEnv.shared.validVersions(for: site.preferredPhpVersion).filter({ version in - version.short != PhpEnv.phpInstall.version.short + guard let install = PhpEnvironments.phpInstall else { + return [] + } + + return PhpEnvironments.shared.validVersions(for: site.preferredPhpVersion).filter({ version in + version.short != install.version.short }) } diff --git a/phpmon/Domain/DomainList/DomainListVC+ContextMenu.swift b/phpmon/Domain/DomainList/DomainListVC+ContextMenu.swift index cea6db6..f806890 100644 --- a/phpmon/Domain/DomainList/DomainListVC+ContextMenu.swift +++ b/phpmon/Domain/DomainList/DomainListVC+ContextMenu.swift @@ -105,7 +105,7 @@ extension DomainListVC { private func addIsolate(to menu: NSMenu, with site: ValetSite) { var items: [NSMenuItem] = [] - for version in PhpEnv.shared.availablePhpVersions.reversed() { + for version in PhpEnvironments.shared.availablePhpVersions.reversed() { let item = PhpMenuItem( title: "domain_list.always_use_php".localized(version), action: #selector(self.isolateSite), @@ -133,7 +133,7 @@ extension DomainListVC { if site.isolatedPhpVersion != nil { menu.addItem(NSMenuItem( - title: "domain_list.use_in_terminal".localized(site.servingPhpVersion), + title: "domain_list.use_in_terminal".localized(site.isolatedPhpVersion!.versionNumber.text), action: #selector(self.useInTerminal) )) } diff --git a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift index 7434fc5..212412e 100644 --- a/phpmon/Domain/Integrations/Composer/ComposerWindow.swift +++ b/phpmon/Domain/Integrations/Composer/ComposerWindow.swift @@ -27,7 +27,7 @@ import Foundation return } - PhpEnv.shared.isBusy = true + PhpEnvironments.shared.isBusy = true MainMenu.shared.setBusyImage() MainMenu.shared.rebuild() @@ -105,7 +105,7 @@ import Foundation // MARK: Main Menu Update private func removeBusyStatus() { - PhpEnv.shared.isBusy = false + PhpEnvironments.shared.isBusy = false Task { @MainActor in MainMenu.shared.updatePhpVersionInStatusBar() } diff --git a/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift b/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift new file mode 100644 index 0000000..3ab3b87 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Behaviors/BrewPermissionFixer.swift @@ -0,0 +1,125 @@ +// +// BrewPermissionFixer.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 23/04/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class BrewPermissionFixer { + + var broken: [DueOwnershipFormula] = [] + + /** + Takes ownership of the /BREW_PATH/Cellar/php/x.y.z/bin folder, for all PHP versions. + + This might not be required if the user has only used that version of PHP + with site isolation, so this method checks if it's required first. + + This is a required operation for *all* PHP versions when PHP Version Manager is running + operations, since any installation or upgrade may prompt the installation or upgrade + of other PHP versions, in which case the permissions need to set correctly. + */ + public func fixPermissions() async throws { + await determineBrokenFormulae() + + if broken.isEmpty { + return + } + + let appleScript = NSAppleScript( + source: "do shell script \"\(buildBrokenFormulaeScript())\" with administrator privileges" + ) + + let eventResult: NSAppleEventDescriptor? = appleScript? + .executeAndReturnError(nil) + + if eventResult == nil { + throw HomebrewPermissionError( + kind: .applescriptNilError + ) + } + + Log.info("Ownership was taken of the folder(s) at: " + broken + .map({ $0.path }) + .joined(separator: ", ")) + } + + /** + Determines which formulae's permissions are broken. + + To do so, PHP Monitor resolves which directory needs to be checked and verifies + whether the Homebrew binary directory for the given PHP version is owned by root. + */ + private func determineBrokenFormulae() async { + let formulae = PhpEnvironments.shared.cachedPhpInstallations.keys + + for formula in formulae { + let realFormula = formula == PhpEnvironments.brewPhpAlias + ? "php" + : "php@\(formula)" + + let binFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/bin") + let sbinFolderOwned = isOwnedByRoot(path: "\(Paths.optPath)/\(realFormula)/sbin") + + if binFolderOwned || sbinFolderOwned { + Log.warn("\(formula) is owned by root") + + if binFolderOwned { + broken.append(DueOwnershipFormula( + formula: realFormula, + path: "\(Paths.optPath)/\(realFormula)/bin" + )) + } + + if sbinFolderOwned { + broken.append(DueOwnershipFormula( + formula: realFormula, + path: "\(Paths.optPath)/\(realFormula)/sbin" + )) + } + } + } + } + + /** + Generates the appropriate AppleScript script required to restore permissions. + This script also stops the services prior to taking ownership, which is requirement. + */ + private func buildBrokenFormulaeScript() -> String { + return broken + .map { b in + return """ + \(Paths.brew) services stop \(b.formula) \ + && chown -R \(Paths.whoami):admin \(b.path) + """ + } + .joined( + separator: " && " + ) + } + + /** + Checks if the directory at the path is owned by the `root` user, + by checking the FS owner account name attribute. + */ + private func isOwnedByRoot(path: String) -> Bool { + do { + let attributes = try FileManager.default.attributesOfItem(atPath: path) + if let owner = attributes[.ownerAccountName] as? String { + return owner == "root" + } + } catch { + return true + } + + return true + } + + struct DueOwnershipFormula { + let formula: String + let path: String + } +} diff --git a/phpmon/Domain/Integrations/Homebrew/Brew.swift b/phpmon/Domain/Integrations/Homebrew/Brew.swift new file mode 100644 index 0000000..5d94ac8 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Brew.swift @@ -0,0 +1,58 @@ +// +// Homebrew.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 17/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class BrewFormulaeObservable: ObservableObject { + @Published var phpVersions: [BrewFormula] = [] + + var upgradeable: [BrewFormula] { + return phpVersions.filter { formula in + formula.hasUpgrade + } + } +} + +class Brew { + static let shared = Brew() + + /// Formulae that can be observed. + var formulae = BrewFormulaeObservable() + + /// The version of Homebrew that was detected. + var version: VersionNumber? + + /// Determine which version of Homebrew is installed. + public func determineVersion() async { + let output = await Shell.pipe("\(Paths.brew) --version") + self.version = try? VersionNumber.parse(output.out) + + if let version = version { + Log.info("The user has Homebrew \(version.text) installed.") + + if version.major < 4 { + Log.warn("Managing PHP versions is only officially supported with Homebrew 4 or newer!") + } + } else { + Log.warn("The Homebrew version could not be determined.") + } + } + + /// Each formula for each PHP version that can be installed. + public static let phpVersionFormulae = [ + "8.2": "php@8.2", + "8.1": "php@8.1", + "8.0": "php@8.0", + "7.4": "shivammathur/php/php@7.4", + "7.3": "shivammathur/php/php@7.3", + "7.2": "shivammathur/php/php@7.2", + "7.1": "shivammathur/php/php@7.1", + "7.0": "shivammathur/php/php@7.0", + "5.6": "shivammathur/php/php@5.6" + ] +} diff --git a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift b/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift similarity index 81% rename from phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift rename to phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift index 6de663d..1141a62 100644 --- a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift +++ b/phpmon/Domain/Integrations/Homebrew/BrewDiagnostics.swift @@ -1,5 +1,5 @@ // -// AliasConflict.swift +// BrewDiagnostics.swift // PHP Monitor // // Created by Nico Verbruggen on 28/11/2021. @@ -8,7 +8,7 @@ import Foundation -class HomebrewDiagnostics { +class BrewDiagnostics { /** Determines the Homebrew taps the user has installed. */ @@ -63,30 +63,25 @@ class HomebrewDiagnostics { It is possible to upgrade PHP, but forget running `valet install`. This results in a scenario where a rogue www.conf file exists. */ - public static func checkForPhpFpmPoolConflicts() { - Log.info("Checking for PHP-FPM pool conflicts...") + public static func checkForValetMisconfiguration() async { + Log.info("Checking for PHP-FPM issues with Valet...") + + guard let install = PhpEnvironments.phpInstall else { + Log.info("Will skip check for issues if no PHP version is linked.") + return + } // We'll need to know what the primary PHP version is - let primary = PhpEnv.shared.currentInstall.version.short + let primary = install.version.short // Versions to be handled let switcher = InternalSwitcher() - var versions = switcher.getVersionsToBeHandled(primary) - versions = versions.filter { version in - return switcher.requiresDisablingOfDefaultPhpFpmPool(version) - } - - if versions.isEmpty { - Log.info("No PHP-FPM pools need to be fixed. All OK.") - } - - versions.forEach { version in - Task { // Fix each pool concurrently (but perform the tasks sequentially) - await switcher.disableDefaultPhpFpmPool(version) - await switcher.stopPhpVersion(version) - await switcher.startPhpVersion(version, primary: version == primary) - } + for version in switcher.getVersionsToBeHandled(primary) + where await switcher.ensureValetConfigurationIsValidForPhpVersion(version) { + Log.info("One or more fixes were applied for PHP \(version)!") + await switcher.unlinkAndStopPhpVersion(version) + await switcher.linkAndStartPhpVersion(version, primary: version == primary) } } @@ -108,13 +103,13 @@ class HomebrewDiagnostics { from: tapAlias.data(using: .utf8)! ).first! - if tapPhp.version != PhpEnv.brewPhpAlias { + if tapPhp.version != PhpEnvironments.brewPhpAlias { Log.warn("The `php` formula alias seems to be the different between the tap and core. " + "This could be a problem!") Log.info("Determining whether both of these versions are installed...") - let bothInstalled = PhpEnv.shared.availablePhpVersions.contains(tapPhp.version) - && PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpAlias) + let bothInstalled = PhpEnvironments.shared.availablePhpVersions.contains(tapPhp.version) + && PhpEnvironments.shared.availablePhpVersions.contains(PhpEnvironments.brewPhpAlias) if bothInstalled { Log.warn("Both conflicting aliases seem to be installed, warning the user!") diff --git a/phpmon/Domain/Integrations/Homebrew/BrewFormula.swift b/phpmon/Domain/Integrations/Homebrew/BrewFormula.swift new file mode 100644 index 0000000..140cae0 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/BrewFormula.swift @@ -0,0 +1,69 @@ +// +// BrewFormula.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 17/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct BrewFormula { + /// Name of the formula. + let name: String + + /// The human readable name for this formula. + let displayName: String + + /// The version of the formula that is currently installed. + let installedVersion: String? + + /// The upgrade that is currently available, if it exists. + let upgradeVersion: String? + + /// Whether the formula is currently installed. + var isInstalled: Bool { + return installedVersion != nil + } + + /// Whether the formula can be upgraded. + var hasUpgrade: Bool { + return upgradeVersion != nil + } + + /// The associated Homebrew folder with this PHP formula. + var homebrewFolder: String { + let resolved = name + .replacingOccurrences(of: "shivammathur/php/", with: "") + .replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php") + + return "\(Paths.optPath)/\(resolved)/bin" + } + + /// The short version associated with this formula, if installed. + var shortVersion: String? { + guard let version = self.installedVersion else { + return nil + } + + return VersionNumber.make(from: version)?.short ?? nil + } + + /// A quick variable that you can check to see if the install is unhealthy. + /// Will report true if no health information is available. + var healthy: Bool { + return isHealthy() ?? true + } + + /** + * Determines if this PHP installation is healthy. + * Uses the cached installation health check as basis. + */ + public func isHealthy() -> Bool? { + guard let shortVersion = self.shortVersion else { + return nil + } + + return PhpEnvironments.shared.cachedPhpInstallations[shortVersion]?.isHealthy ?? nil + } +} diff --git a/phpmon/Domain/Integrations/Homebrew/BrewFormulaeHandler.swift b/phpmon/Domain/Integrations/Homebrew/BrewFormulaeHandler.swift new file mode 100644 index 0000000..9fb0cd4 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/BrewFormulaeHandler.swift @@ -0,0 +1,63 @@ +// +// BrewFormulaeHandler.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +protocol HandlesBrewFormulae { + func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] + func refreshPhpVersions(loadOutdated: Bool) async +} + +extension HandlesBrewFormulae { + public func refreshPhpVersions(loadOutdated: Bool) async { + let items = await loadPhpVersions(loadOutdated: loadOutdated) + Task { @MainActor in + Brew.shared.formulae.phpVersions = items + } + } +} + +class BrewFormulaeHandler: HandlesBrewFormulae { + public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] { + var outdated: [OutdatedFormula]? + + if loadOutdated { + let command = """ + \(Paths.brew) update >/dev/null && \ + \(Paths.brew) outdated --json --formulae + """ + + let rawJsonText = await Shell.pipe(command).out + .data(using: .utf8)! + outdated = try? JSONDecoder().decode( + OutdatedFormulae.self, + from: rawJsonText + ).formulae.filter({ formula in + formula.name.starts(with: "php") + }) + } + + return Brew.phpVersionFormulae.map { (version, formula) in + let fullVersion = PhpEnvironments.shared.cachedPhpInstallations[version]?.versionNumber.text + var upgradeVersion: String? + + if let version = fullVersion { + upgradeVersion = outdated?.first(where: { formula in + return formula.installed_versions.contains(version) + })?.current_version + } + + return BrewFormula( + name: formula, + displayName: "PHP \(version)", + installedVersion: fullVersion, + upgradeVersion: upgradeVersion + ) + }.sorted { $0.displayName > $1.displayName } + } +} diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift new file mode 100644 index 0000000..2f67281 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Commands/BrewCommand.swift @@ -0,0 +1,49 @@ +// +// 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(onProgress: @escaping (BrewCommandProgress) -> Void) async throws +} + +extension BrewCommand { + internal func reportInstallationProgress(_ text: String) -> (Double, String)? { + if text.contains("Fetching") { + return (0.1, "phpman.steps.fetching".localized) + } + if text.contains("Downloading") { + return (0.25, "phpman.steps.downloading".localized) + } + if text.contains("Installing") { + return (0.60, "phpman.steps.installing".localized) + } + if text.contains("Pouring") { + return (0.80, "phpman.steps.pouring".localized) + } + if text.contains("Summary") { + return (0.90, "phpman.steps.summary".localized) + } + return nil + } +} + +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] +} diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift new file mode 100644 index 0000000..20ed421 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Commands/InstallAndUpgradeCommand.swift @@ -0,0 +1,172 @@ +// +// HomebrewOperationManager.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 28/04/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class InstallAndUpgradeCommand: BrewCommand { + + let title: String + let installing: [BrewFormula] + let upgrading: [BrewFormula] + let phpGuard: PhpGuard + + /** + You can pass in which PHP versions need to be upgraded and which ones need to be installed. + The process will be executed in two steps: first upgrades, then installations. + Upgrades come first because... well, otherwise installations may very well break. + Each version that is installed will need to be checked afterwards (if it is OK). + */ + public init( + title: String, + upgrading: [BrewFormula], + installing: [BrewFormula] + ) { + self.title = title + self.installing = installing + self.upgrading = upgrading + self.phpGuard = PhpGuard() + } + + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + let progressTitle = "Please wait..." + + onProgress(.create( + value: 0.2, + title: progressTitle, + description: "PHP Monitor is preparing Homebrew..." + )) + + // Try to run all upgrade and installation operations + try await self.upgradePackages(onProgress) + try await self.installPackages(onProgress) + + // Re-check the installed versions + await PhpEnvironments.detectPhpVersions() + + // After performing operations, attempt to run repairs if needed + try await self.repairBrokenPackages(onProgress) + + // Finally, complete all operations + await self.completedOperations(onProgress) + } + + private func upgradePackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + // If no upgrades are needed, early exit + if self.upgrading.isEmpty { + return + } + + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + \(Paths.brew) upgrade \(self.upgrading.map { $0.name }.joined(separator: " ")) + """ + + try await run(command, onProgress) + } + + private func installPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + // If no installations are needed, early exit + if self.installing.isEmpty { + return + } + + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + \(Paths.brew) install \(self.installing.map { $0.name }.joined(separator: " ")) --force + """ + + try await run(command, onProgress) + } + + private func repairBrokenPackages(_ onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + // Determine which PHP installations are considered unhealthy + // Build a list of formulae to reinstall + let requiringRepair = PhpEnvironments.shared + .cachedPhpInstallations.values + .filter({ !$0.isHealthy }) + .map { installation in + let formula = "php@\(installation.versionNumber.short)" + + if installation.versionNumber.short == PhpEnvironments.brewPhpAlias { + return "php" + } + + return formula + } + + // If no repairs are needed, early exit + if requiringRepair.isEmpty { + return + } + + // If the health comes back as negative, attempt to reinstall + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + export HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=true; \ + \(Paths.brew) reinstall \(requiringRepair.joined(separator: " ")) --force + """ + + try await run(command, onProgress) + } + + private func run(_ 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: self.title, description: text)) + } + }, + withTimeout: .minutes(15) + ) + + if process.terminationStatus <= 0 { + loggedMessages = [] + return + } else { + throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages) + } + } + + private func completedOperations(_ onProgress: @escaping (BrewCommandProgress) -> Void) async { + // Reload and restart PHP versions + onProgress(.create(value: 0.95, title: self.title, description: "Reloading PHP versions...")) + + // Check which version of PHP are now installed + await PhpEnvironments.detectPhpVersions() + + // Keep track of the currently installed version + await MainMenu.shared.refreshActiveInstallation() + + // If a PHP version was active prior to running the operations, attempt to restore it + if let version = phpGuard.currentVersion { + await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true) + } + + // Also rebuild the content of the main menu + await MainMenu.shared.rebuild() + + // Let the UI know that the installation has been completed + onProgress(.create( + value: 1, + title: "Operation completed!", + description: "The installation has succeeded." + )) + } + +} diff --git a/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift new file mode 100644 index 0000000..e1d3612 --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Commands/RemovePhpVersionCommand.swift @@ -0,0 +1,74 @@ +// +// RemovePhpVersionCommand.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class RemovePhpVersionCommand: BrewCommand { + let formula: String + let version: String + let phpGuard: PhpGuard + + init(formula: String) { + self.version = formula + .replacingOccurrences(of: "php@", with: "") + .replacingOccurrences(of: "shivammathur/php/", with: "") + self.formula = formula + self.phpGuard = PhpGuard() + } + + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + let progressTitle = "Removing PHP \(version)..." + + onProgress(.create( + value: 0.2, + title: progressTitle, + description: "Please wait while Homebrew removes PHP \(version)..." + )) + + let command = """ + export HOMEBREW_NO_INSTALL_UPGRADE=true; \ + export HOMEBREW_NO_INSTALL_CLEANUP=true; \ + \(Paths.brew) remove \(formula) --force --ignore-dependencies + """ + + do { + try await BrewPermissionFixer().fixPermissions() + } catch { + return + } + + var loggedMessages: [String] = [] + + let (process, _) = try! await Shell.attach( + command, + didReceiveOutput: { text, _ in + if !text.isEmpty { + Log.perf(text) + loggedMessages.append(text) + } + }, + withTimeout: .minutes(5) + ) + + if process.terminationStatus <= 0 { + onProgress(.create(value: 0.95, title: progressTitle, description: "Reloading PHP versions...")) + + await PhpEnvironments.detectPhpVersions() + + await MainMenu.shared.refreshActiveInstallation() + + if let version = phpGuard.currentVersion { + await MainMenu.shared.switchToPhpVersionAndWait(version, silently: true) + } + + onProgress(.create(value: 1, title: progressTitle, description: "The operation has succeeded.")) + } else { + throw BrewCommandError(error: "The command failed to run correctly.", log: loggedMessages) + } + } +} diff --git a/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift new file mode 100644 index 0000000..ebf704a --- /dev/null +++ b/phpmon/Domain/Integrations/Homebrew/Fake/FakeCommand.swift @@ -0,0 +1,25 @@ +// +// FakeCommand.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class FakeCommand: BrewCommand { + let version: String + + init(version: String) { + self.version = version + } + + func execute(onProgress: @escaping (BrewCommandProgress) -> Void) async throws { + onProgress(.create(value: 0.2, title: "Hello", description: "Doing the work")) + await delay(seconds: 2) + onProgress(.create(value: 0.5, title: "Hello", description: "Doing some more work")) + await delay(seconds: 1) + onProgress(.create(value: 1, title: "Hello", description: "Job's done")) + } +} diff --git a/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift b/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift index 9817111..b312449 100644 --- a/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift +++ b/phpmon/Domain/Integrations/Valet/Domains/FakeValetInteractor.swift @@ -75,7 +75,7 @@ class FakeValetInteractor: ValetInteractor { override func isolate(site: ValetSite, version: String) async throws { await delay(seconds: delayTime) - site.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version] + site.isolatedPhpVersion = PhpEnvironments.shared.cachedPhpInstallations[version] site.evaluateCompatibility() } diff --git a/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift index b866f8c..8849d07 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/FakeValetSite.swift @@ -42,7 +42,7 @@ class FakeValetSite: ValetSite { self.isolatedPhpVersion = PhpInstallation(isolated) } - if PhpEnv.shared.currentInstall != nil { + if PhpEnvironments.shared.currentInstall != nil { self.evaluateCompatibility() } } diff --git a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift index baeff52..0ba1610 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift @@ -57,7 +57,8 @@ class ValetSite: ValetListable { /// Which version of PHP is actually used to serve this site. var servingPhpVersion: String { return self.isolatedPhpVersion?.versionNumber.short - ?? PhpEnv.phpInstall.version.short + ?? PhpEnvironments.phpInstall?.version.short + ?? "???" } init( @@ -97,12 +98,12 @@ class ValetSite: ValetListable { */ public func determineIsolated() { if let version = ValetSite.isolatedVersion("~/.config/valet/Nginx/\(self.name).\(self.tld)") { - if !PhpEnv.shared.cachedPhpInstallations.keys.contains(version) { + if !PhpEnvironments.shared.cachedPhpInstallations.keys.contains(version) { Log.err("The PHP version \(version) is isolated for the site \(self.name) " + "but that PHP version is unavailable.") return } - self.isolatedPhpVersion = PhpEnv.shared.cachedPhpInstallations[version] + self.isolatedPhpVersion = PhpEnvironments.shared.cachedPhpInstallations[version] } else { self.isolatedPhpVersion = nil } @@ -237,11 +238,16 @@ class ValetSite: ValetListable { return } + guard let linked = PhpEnvironments.phpInstall else { + self.isCompatibleWithPreferredPhpVersion = false + return + } + // Split the composer list (on "|") to evaluate multiple constraints // For example, for Laravel 8 projects the value is "^7.3|^8.0" self.isCompatibleWithPreferredPhpVersion = self.preferredPhpVersion.split(separator: "|").map { string in let origin = self.isolatedPhpVersion?.versionNumber.short - ?? PhpEnv.phpInstall.version.long + ?? linked.version.long let normalizedPhpVersion = string.trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift new file mode 100644 index 0000000..3f722bc --- /dev/null +++ b/phpmon/Domain/Integrations/Valet/Valet+Alerts.swift @@ -0,0 +1,74 @@ +// +// ActivePhpInstallation.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/12/2021. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +extension Valet { + + /** + Notify the user about a non-default TLD being set. + */ + public func notifyAboutUnsupportedTLD() { + if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) { + Task { @MainActor in + BetterAlert().withInformation( + title: "alert.warnings.tld_issue.title".localized, + subtitle: "alert.warnings.tld_issue.subtitle".localized, + description: "alert.warnings.tld_issue.description".localized + ) + .withPrimary(text: "generic.ok".localized) + .withTertiary(text: "alert.do_not_tell_again".localized, action: { alert in + Preferences.update(.warnAboutNonStandardTLD, value: false) + alert.close(with: .alertThirdButtonReturn) + }) + .show() + } + } + } + + public func notifyAboutOutdatedValetVersion(_ version: VersionNumber) { + Task { @MainActor in + BetterAlert() + .withInformation( + title: "alert.min_valet_version.title".localized, + subtitle: "alert.min_valet_version.info".localized( + version.text, + Constants.MinimumRecommendedValetVersion + ) + ) + .withPrimary(text: "generic.ok".localized) + .show() + } + } + + /** + It is always possible that the system configuration for PHP-FPM has not been set up for Valet. + This can occur when a user manually installs a new PHP version, but does not run `valet install`. + In that case, we should alert the user! + + - Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times. + This method actively presents a modal if said checks fails, so don't call this method too many times. + */ + public func notifyAboutBrokenPhpFpm() async { + if await Valet.shared.phpFpmConfigurationValid() { + return + } + + Task { @MainActor in + BetterAlert() + .withInformation( + title: "alert.php_fpm_broken.title".localized, + subtitle: "alert.php_fpm_broken.info".localized, + description: "alert.php_fpm_broken.description".localized + ) + .withPrimary(text: "generic.ok".localized) + .show() + } + } + +} diff --git a/phpmon/Domain/Integrations/Valet/Valet.swift b/phpmon/Domain/Integrations/Valet/Valet.swift index 61393ef..f9ba951 100644 --- a/phpmon/Domain/Integrations/Valet/Valet.swift +++ b/phpmon/Domain/Integrations/Valet/Valet.swift @@ -22,7 +22,7 @@ class Valet { static let shared = Valet() /// The version of Valet that was detected. - var version: VersionNumber! = nil + var version: VersionNumber? /// The Valet configuration file. var config: Valet.Configuration! @@ -57,6 +57,15 @@ class Valet { } } + static var installed: Bool { + return self.shared.installed + } + + lazy var installed: Bool = { + return FileSystem.fileExists(Paths.binPath.appending("/valet")) + && FileSystem.anyExists("~/.config/valet") + }() + /** Check if a particular feature is enabled. */ @@ -71,27 +80,6 @@ class Valet { return self.shared.sites + self.shared.proxies } - /** - Notify the user about a non-default TLD being set. - */ - public static func notifyAboutUnsupportedTLD() { - if Valet.shared.config.tld != "test" && Preferences.isEnabled(.warnAboutNonStandardTLD) { - Task { @MainActor in - BetterAlert().withInformation( - title: "alert.warnings.tld_issue.title".localized, - subtitle: "alert.warnings.tld_issue.subtitle".localized, - description: "alert.warnings.tld_issue.description".localized - ) - .withPrimary(text: "generic.ok".localized) - .withTertiary(text: "alert.do_not_tell_again".localized, action: { alert in - Preferences.update(.warnAboutNonStandardTLD, value: false) - alert.close(with: .alertThirdButtonReturn) - }) - .show() - } - } - } - /** We don't want to load the initial config.json file as soon as the class is initialised. @@ -142,6 +130,11 @@ class Valet { in use. This allows PHP Monitor to do different things when Valet 3.0 is enabled. */ public func evaluateFeatureSupport() { + guard let version = self.version else { + Log.err("Cannot determine features, as the version was not determined.") + return + } + switch version.major { case 2: Log.info("You are running Valet v2. Support for site isolation is disabled.") @@ -159,26 +152,24 @@ class Valet { installed is not recent enough. */ public func validateVersion() { + guard let version = self.version else { + Log.err("Cannot validate Valet version if no Valet version was determined.") + return + } + + if PhpEnvironments.phpInstall == nil { + Log.info("Cannot validate Valet version if no PHP version is linked.") + return + } + // 1. Evaluate feature support Valet.shared.evaluateFeatureSupport() // 2. Notify user if the version is too old (but major version is OK) if version.text.versionCompare(Constants.MinimumRecommendedValetVersion) == .orderedAscending { - let version = version! let recommended = Constants.MinimumRecommendedValetVersion Log.warn("Valet version \(version.text) is too old! (recommended: \(recommended))") - Task { @MainActor in - BetterAlert() - .withInformation( - title: "alert.min_valet_version.title".localized, - subtitle: "alert.min_valet_version.info".localized( - version.text, - Constants.MinimumRecommendedValetVersion - ) - ) - .withPrimary(text: "generic.ok".localized) - .show() - } + self.notifyAboutOutdatedValetVersion(version) } else { Log.info("Valet version \(version.text) is recent enough, OK " + "(recommended: \(Constants.MinimumRecommendedValetVersion))") @@ -193,6 +184,30 @@ class Valet { .out.contains("Composer detected issues in your platform") } + /** + Determine if PHP-FPM is configured correctly. + + For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent + versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, + that means that Valet won't work properly. + */ + func phpFpmConfigurationValid() async -> Bool { + guard let version = PhpEnvironments.shared.currentInstall?.version else { + Log.info("Cannot check PHP-FPM status: no version of PHP is active") + return true + } + + if version.short == "5.6" { + // The main PHP config file should contain `valet.sock` and then we're probably fine? + let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" + return await Shell.pipe("cat \(fileName)").out + .contains("valet.sock") + } + + // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) + return FileSystem.fileExists("\(Paths.etcPath)/php/\(version.short)/php-fpm.d/valet-fpm.conf") + } + /** Returns a count of how many sites are linked and parked. */ diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift index 690c06f..c89bdeb 100644 --- a/phpmon/Domain/Menu/MainMenu+Actions.swift +++ b/phpmon/Domain/Menu/MainMenu+Actions.swift @@ -12,6 +12,25 @@ extension MainMenu { // MARK: - Actions + @MainActor @objc func linkPhpBinary() { + Task { + await Actions.linkPhp() + } + } + + @MainActor @objc func displayUnlinkedInfo() { + Task { @MainActor in + BetterAlert() + .withInformation( + title: "phpman.unlinked.title".localized, + subtitle: "phpman.unlinked.desc".localized, + description: "phpman.unlinked.detail".localized + ) + .withPrimary(text: "generic.ok".localized) + .show() + } + } + @MainActor @objc func fixHomebrewPermissions() { if !BetterAlert() .withInformation( @@ -86,7 +105,7 @@ extension MainMenu { } @objc func disableAllXdebugModes() { - guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else { Log.info("xdebug.mode could not be found in any .ini file, aborting.") return } @@ -105,7 +124,7 @@ extension MainMenu { @objc func toggleXdebugMode(sender: XdebugMenuItem) { Log.info("Switching Xdebug to mode: \(sender.mode)") - guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + guard let file = PhpEnvironments.shared.getConfigFile(forKey: "xdebug.mode") else { return Log.info("xdebug.mode could not be found in any .ini file, aborting.") } @@ -207,12 +226,17 @@ extension MainMenu { } @objc func openActiveConfigFolder() { - if PhpEnv.phpInstall.hasErrorState { + guard let install = PhpEnvironments.phpInstall else { + // TODO: Can't open the config if no PHP version is active + return + } + + if install.hasErrorState { Actions.openGenericPhpConfigFolder() return } - Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short) + Actions.openPhpConfigFolder(version: install.version.short) } @objc func openPhpMonitorConfigurationFile() { @@ -231,8 +255,11 @@ extension MainMenu { self.switchToPhpVersion(sender.version) } - public func switchToAnyPhpVersion(_ version: String) { - if PhpEnv.shared.availablePhpVersions.contains(version) { + public func switchToAnyPhpVersion(_ version: String, silently: Bool = false) { + if silently { + MainMenu.shared.shouldSwitchSilently = true + } + if PhpEnvironments.shared.availablePhpVersions.contains(version) { Task { MainMenu.shared.switchToPhpVersion(version) } } else { Task { @@ -246,20 +273,44 @@ extension MainMenu { } } + func switchToPhpVersionAndWait(_ version: String, silently: Bool = false) async { + if silently { + MainMenu.shared.shouldSwitchSilently = true + } + + if !PhpEnvironments.shared.availablePhpVersions.contains(version) { + Log.warn("This PHP version is currently unavailable, not switching!") + return + } + + setBusyImage() + PhpEnvironments.shared.isBusy = true + PhpEnvironments.shared.delegate = self + PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version) + + updatePhpVersionInStatusBar() + rebuild() + await PhpEnvironments.switcher.performSwitch(to: version) + + PhpEnvironments.shared.currentInstall = ActivePhpInstallation() + App.shared.handlePhpConfigWatcher() + PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version) + } + @objc func switchToPhpVersion(_ version: String) { setBusyImage() - PhpEnv.shared.isBusy = true - PhpEnv.shared.delegate = self - PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) + PhpEnvironments.shared.isBusy = true + PhpEnvironments.shared.delegate = self + PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version) Task(priority: .userInitiated) { [unowned self] in updatePhpVersionInStatusBar() rebuild() - await PhpEnv.switcher.performSwitch(to: version) + await PhpEnvironments.switcher.performSwitch(to: version) - PhpEnv.shared.currentInstall = ActivePhpInstallation() + PhpEnvironments.shared.currentInstall = ActivePhpInstallation() App.shared.handlePhpConfigWatcher() - PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) + PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version) } } @@ -275,18 +326,18 @@ extension MainMenu { func switchToPhp(_ version: String) async { Task { @MainActor [self] in setBusyImage() - PhpEnv.shared.isBusy = true - PhpEnv.shared.delegate = self - PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) + PhpEnvironments.shared.isBusy = true + PhpEnvironments.shared.delegate = self + PhpEnvironments.shared.delegate?.switcherDidStartSwitching(to: version) } updatePhpVersionInStatusBar() rebuild() - await PhpEnv.switcher.performSwitch(to: version) + await PhpEnvironments.switcher.performSwitch(to: version) - PhpEnv.shared.currentInstall = ActivePhpInstallation() + PhpEnvironments.shared.currentInstall = ActivePhpInstallation() App.shared.handlePhpConfigWatcher() - PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) + PhpEnvironments.shared.delegate?.switcherDidCompleteSwitch(to: version) } } diff --git a/phpmon/Domain/Menu/MainMenu+Async.swift b/phpmon/Domain/Menu/MainMenu+Async.swift index e81e688..c610a7c 100644 --- a/phpmon/Domain/Menu/MainMenu+Async.swift +++ b/phpmon/Domain/Menu/MainMenu+Async.swift @@ -46,7 +46,7 @@ extension MainMenu { ] ) { if behaviours.contains(.reloadsPhpInstallation) { - PhpEnv.shared.isBusy = true + PhpEnvironments.shared.isBusy = true } if behaviours.contains(.setsBusyUI) { @@ -59,12 +59,12 @@ extension MainMenu { do { try execute() } catch let e { error = e } if behaviours.contains(.setsBusyUI) { - PhpEnv.shared.isBusy = false + PhpEnvironments.shared.isBusy = false } Task { @MainActor [self, error] in if behaviours.contains(.reloadsPhpInstallation) { - PhpEnv.shared.currentInstall = ActivePhpInstallation() + PhpEnvironments.shared.currentInstall = ActivePhpInstallation() } if behaviours.contains(.updatesMenuBarContents) { diff --git a/phpmon/Domain/Menu/MainMenu+FixMyValet.swift b/phpmon/Domain/Menu/MainMenu+FixMyValet.swift index ea69271..d685c9a 100644 --- a/phpmon/Domain/Menu/MainMenu+FixMyValet.swift +++ b/phpmon/Domain/Menu/MainMenu+FixMyValet.swift @@ -12,9 +12,9 @@ import AppKit extension MainMenu { @MainActor @objc func fixMyValet() { - let previousVersion = PhpEnv.phpInstall.version.short + let previousVersion = PhpEnvironments.phpInstall?.version.short - if !PhpEnv.shared.availablePhpVersions.contains(PhpEnv.brewPhpAlias) { + if !PhpEnvironments.shared.availablePhpVersions.contains(PhpEnvironments.brewPhpAlias) { presentAlertForMissingFormula() return } @@ -22,7 +22,7 @@ extension MainMenu { if !BetterAlert() .withInformation( title: "alert.fix_my_valet.title".localized, - subtitle: "alert.fix_my_valet.info".localized(PhpEnv.brewPhpAlias) + subtitle: "alert.fix_my_valet.info".localized(PhpEnvironments.brewPhpAlias) ) .withPrimary(text: "alert.fix_my_valet.ok".localized) .withSecondary(text: "alert.fix_my_valet.cancel".localized) @@ -34,10 +34,10 @@ extension MainMenu { Task { @MainActor in await Actions.fixMyValet() - if previousVersion == PhpEnv.brewPhpAlias { + if previousVersion == PhpEnvironments.brewPhpAlias || previousVersion == nil { self.presentAlertForSameVersion() } else { - self.presentAlertForDifferentVersion(version: previousVersion) + self.presentAlertForDifferentVersion(version: previousVersion!) } } } @@ -74,7 +74,7 @@ extension MainMenu { alert.close(with: .alertSecondButtonReturn) MainMenu.shared.switchToPhpVersion(version) }) - .withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnv.brewPhpAlias)) + .withSecondary(text: "alert.fix_my_valet_done.stay".localized(PhpEnvironments.brewPhpAlias)) .withTertiary(text: "", action: { _ in NSWorkspace.shared.open(Constants.Urls.FrequentlyAskedQuestions) }) diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 8801296..8d06353 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -18,8 +18,6 @@ extension MainMenu { self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) } - await App.shared.environment.process() - if await Startup().checkEnvironment() { await self.onEnvironmentPass() } else { @@ -32,18 +30,18 @@ extension MainMenu { */ private func onEnvironmentPass() async { // Determine what the `php` formula is aliased to - await PhpEnv.shared.determinePhpAlias() + await PhpEnvironments.shared.determinePhpAlias() // Initialize preferences _ = Preferences.shared // Determine install method - Log.info(HomebrewDiagnostics.customCaskInstalled + Log.info(BrewDiagnostics.customCaskInstalled ? "[BREW] The app has been installed via Homebrew Cask." : "[BREW] The app has been installed directly (optimal)." ) - Log.info(HomebrewDiagnostics.usesNginxFullFormula + Log.info(BrewDiagnostics.usesNginxFullFormula ? "[BREW] The app will be using the `nginx-full` formula." : "[BREW] The app will be using the `nginx` formula." ) @@ -51,24 +49,28 @@ extension MainMenu { // Attempt to find out more info about Valet if Valet.shared.version != nil { Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!.text)") + + // Validate the version (this will enforce which versions of PHP are supported) + Valet.shared.validateVersion() } - // Validate the version (this will enforce which versions of PHP are supported) - Valet.shared.validateVersion() + // Validate the Homebrew version (determines install/upgrade functionality) + await Brew.shared.determineVersion() // Actually detect the PHP versions - await PhpEnv.detectPhpVersions() + await PhpEnvironments.detectPhpVersions() // Check for an alias conflict - await HomebrewDiagnostics.checkForCaskConflict() + await BrewDiagnostics.checkForCaskConflict() // Update the icon updatePhpVersionInStatusBar() // Attempt to find out if PHP-FPM is broken - Log.info("Determining broken PHP-FPM...") - let installation = PhpEnv.phpInstall - installation.notifyAboutBrokenPhpFpm() + PhpEnvironments.prepare() + + // Set up the filesystem watcher for the Homebrew binaries + App.shared.prepareHomebrewWatchers() // Check for other problems WarningManager.shared.evaluateWarnings() @@ -86,21 +88,26 @@ extension MainMenu { // Load the global hotkey App.shared.loadGlobalHotkey() - // Preload sites - await Valet.shared.startPreloadingSites() + // Set up menu items + AppDelegate.instance.configureMenuItems(standalone: !Valet.installed) - // After preloading sites, check for PHP-FPM pool conflicts - HomebrewDiagnostics.checkForPhpFpmPoolConflicts() + if Valet.installed { + // Preload all sites + await Valet.shared.startPreloadingSites() - // A non-default TLD is not officially supported since Valet 3.2.x - Valet.notifyAboutUnsupportedTLD() + // After preloading sites, check for PHP-FPM pool conflicts + await BrewDiagnostics.checkForValetMisconfiguration() + + // Check if PHP-FPM is broken (should be fixed automatically if phpmon >= 6.0) + await Valet.shared.notifyAboutBrokenPhpFpm() + + // A non-default TLD is not officially supported since Valet 3.2.x + Valet.shared.notifyAboutUnsupportedTLD() + } // Find out which services are active Log.info("The services manager knows about \(ServicesManager.shared.services.count) services.") - // Start the background refresh timer - startSharedTimer() - if !isRunningSwiftUIPreview { Stats.incrementSuccessfulLaunchCount() Stats.evaluateSponsorMessageShouldBeDisplayed() @@ -116,13 +123,13 @@ extension MainMenu { } // Check if the linked version has changed between launches of phpmon - Stats.evaluateLastLinkedPhpVersion() - - // Check if an update was performed earlier - AppUpdater.checkIfUpdateWasPerformed() + PhpGuard().compareToLastGlobalVersion() // We are ready! Log.info("PHP Monitor is ready to serve!") + + // Check if we upgraded just now + AppUpdater.checkIfUpdateWasPerformed() } /** @@ -149,21 +156,6 @@ extension MainMenu { } } - /** - Schedule a request to fetch the PHP version every 60 seconds. - */ - private func startSharedTimer() { - DispatchQueue.main.async { [self] in - App.shared.timer = Timer.scheduledTimer( - timeInterval: 60, - target: self, - selector: #selector(refreshActiveInstallation), - userInfo: nil, - repeats: true - ) - } - } - /** Detect which applications are installed that can be used to open a domain's source directory. */ diff --git a/phpmon/Domain/Menu/MainMenu+Switcher.swift b/phpmon/Domain/Menu/MainMenu+Switcher.swift index c96b75a..cafc6f1 100644 --- a/phpmon/Domain/Menu/MainMenu+Switcher.swift +++ b/phpmon/Domain/Menu/MainMenu+Switcher.swift @@ -16,17 +16,19 @@ extension MainMenu { nonisolated func switcherDidCompleteSwitch(to version: String) { // Mark as no longer busy - PhpEnv.shared.isBusy = false + PhpEnvironments.shared.isBusy = false Task { // Things to do after reloading domain list data - await self.reloadDomainListData() + if Valet.installed { + await self.reloadDomainListData() + } // Perform UI updates on main thread Task { @MainActor [self] in updatePhpVersionInStatusBar() rebuild() - if !PhpEnv.shared.validate(version) { + if Valet.installed && !PhpEnvironments.shared.validate(version) { self.suggestFixMyValet(failed: version) return } @@ -44,7 +46,15 @@ extension MainMenu { } // Check if Valet still works correctly - self.checkForPlatformIssues() + if Valet.installed { + self.checkForPlatformIssues() + } + + // Check if the silent switch occurred and reset it + if shouldSwitchSilently { + shouldSwitchSilently = false + return + } // Update stats Stats.incrementSuccessfulSwitchCount() @@ -112,12 +122,28 @@ extension MainMenu { } @MainActor private func notifyAboutVersionChange(to version: String) { + if shouldSwitchSilently { + return + } + LocalNotification.send( title: String(format: "notification.version_changed_title".localized, version), subtitle: String(format: "notification.version_changed_desc".localized, version), preference: .notifyAboutVersionChange ) - Task { PhpEnv.phpInstall.notifyAboutBrokenPhpFpm() } + guard PhpEnvironments.phpInstall != nil else { + Log.err("Cannot notify about version change if PHP is unlinked") + return + } + + guard Valet.installed == true else { + Log.info("Skipping check for broken PHP-FPM version, Valet is not installed") + return + } + + Task { + await Valet.shared.notifyAboutBrokenPhpFpm() + } } } diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 0c5aa7d..885d999 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -26,6 +26,14 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate withLength: NSStatusItem.variableLength ) + // MARK: - State Variables + + /** + You can instruct the app to switch to a given PHP version silently. + That will toggle this flag to true. Upon switching, this flag will be reset. + */ + var shouldSwitchSilently: Bool = false + // MARK: - UI related /** @@ -70,8 +78,8 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate /** Reloads which PHP versions is currently active. */ @objc func refreshActiveInstallation() { - if !PhpEnv.shared.isBusy { - PhpEnv.shared.currentInstall = ActivePhpInstallation() + if !PhpEnvironments.shared.isBusy { + PhpEnvironments.shared.currentInstall = ActivePhpInstallation.load() updatePhpVersionInStatusBar() } else { Log.perf("Skipping version refresh due to busy status!") @@ -114,7 +122,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate BetterAlert().withInformation( title: "startup.unsupported_versions_explanation.title".localized, subtitle: "startup.unsupported_versions_explanation.subtitle".localized( - PhpEnv.shared.incompatiblePhpVersions + PhpEnvironments.shared.incompatiblePhpVersions .map({ version in return "• PHP \(version)" }) @@ -143,7 +151,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate /** Refreshes the icon with the PHP version. */ @objc func refreshIcon() { Task { @MainActor [self] in - if PhpEnv.shared.isBusy { + if PhpEnvironments.shared.isBusy { setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) } else { if Preferences.preferences[.shouldDisplayDynamicIcon] as! Bool == false { @@ -152,7 +160,13 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } else { // The dynamic icon has been requested let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool - setStatusBarImage(version: long ? PhpEnv.phpInstall.version.long : PhpEnv.phpInstall.version.short) + + guard let install = PhpEnvironments.phpInstall else { + setStatusBarImage(version: "???") + return + } + + setStatusBarImage(version: long ? install.version.long : install.version.short) } } } @@ -172,6 +186,18 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate NSApplication.shared.orderFrontStandardAboutPanel() } + @objc func openLiteModeInfo() { + Task { @MainActor in + BetterAlert().withInformation( + title: "lite_mode_explanation.title".localized, + subtitle: "lite_mode_explanation.subtitle".localized, + description: "lite_mode_explanation.description".localized + ) + .withPrimary(text: "generic.ok".localized) + .show() + } + } + @objc func openPrefs() { PreferencesWindowController.show() } @@ -184,6 +210,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate DomainListVC.show() } + @objc func openPhpVersionManager() { + PhpVersionManagerWindowController.show() + } + @objc func openDonate() { NSWorkspace.shared.open(Constants.Urls.DonationPage) } diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift index 21c9831..fd9ca10 100644 --- a/phpmon/Domain/Menu/StatusMenu+Items.swift +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -13,31 +13,49 @@ import Cocoa extension StatusMenu { func addPhpVersionMenuItems() { - if PhpEnv.phpInstall.hasErrorState { + if PhpEnvironments.phpInstall == nil { + addItem(HeaderView.asMenuItem(text: "⚠️ " + "mi_no_php_linked".localized, minimumWidth: 280)) + addItems([ + NSMenuItem.separator(), + NSMenuItem(title: "mi_fix_php_link".localized, action: #selector(MainMenu.linkPhpBinary)), + NSMenuItem(title: "mi_no_php_linked_explain".localized, action: #selector(MainMenu.displayUnlinkedInfo)) + ]) + return + } + + if PhpEnvironments.phpInstall!.hasErrorState { let brokenMenuItems = ["mi_php_broken_1", "mi_php_broken_2", "mi_php_broken_3", "mi_php_broken_4"] return addItems(brokenMenuItems.map { NSMenuItem(title: $0.localized) }) } addItem(HeaderView.asMenuItem( - text: "\("mi_php_version".localized) \(PhpEnv.phpInstall.version.long)", + text: "\("mi_php_version".localized) \(PhpEnvironments.phpInstall!.version.long)", minimumWidth: 280 // this ensures the menu is at least wide enough not to cause clipping )) } func addPhpActionMenuItems() { - if PhpEnv.shared.isBusy { + if PhpEnvironments.shared.isBusy { addItem(NSMenuItem(title: "mi_busy".localized)) return } - if PhpEnv.shared.availablePhpVersions.isEmpty && PhpEnv.shared.incompatiblePhpVersions.isEmpty { return } + if PhpEnvironments.shared.availablePhpVersions.isEmpty + && PhpEnvironments.shared.incompatiblePhpVersions.isEmpty { + return + } + + if PhpEnvironments.shared.currentInstall == nil { + return + } addSwitchToPhpMenuItems() + self.addItem(NSMenuItem.separator()) } func addServicesManagerMenuItem() { - if PhpEnv.shared.isBusy { + if PhpEnvironments.shared.isBusy { return } @@ -49,19 +67,20 @@ extension StatusMenu { func addSwitchToPhpMenuItems() { var shortcutKey = 1 - for index in (0.. GenericPreferenceVC { + let vc = NSStoryboard(name: "Main", bundle: nil) + .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC + + _ = vc.addView(when: true, vc.getShowPhpDoctorSuggestionsPV()) + .addView(when: true, vc.getAutoRestartServicesPV()) + .addView(when: true, vc.getAutomaticComposerUpdatePV()) + .addView(when: true, vc.getShortcutPV()) + .addView(when: true, vc.getIntegrationsPV()) + .addView(when: true, vc.getAutomaticUpdateCheckPV()) + + if #available(macOS 13, *) { + vc.views.append(CheckboxPreferenceView.makeLoginItemView()) + } + + return vc + } +} + +class AppearancePreferencesVC: GenericPreferenceVC { + + public static func fromStoryboard() -> GenericPreferenceVC { + let vc = NSStoryboard(name: "Main", bundle: nil) + .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC + + _ = vc.addView(when: true, vc.getDynamicIconPV()) + .addView(when: true, vc.getIconOptionsPV()) + .addView(when: true, vc.getIconDensityPV()) + + return vc + } +} + +class MenuStructurePreferencesVC: GenericPreferenceVC { + + // swiftlint:disable line_length + public static func fromStoryboard() -> GenericPreferenceVC { + let vc = NSStoryboard(name: "Main", bundle: nil) + .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC + + _ = vc.addView(when: true, vc.displayFeature("prefs.display_global_version_switcher", .displayGlobalVersionSwitcher, true)) + .addView(when: Valet.installed, vc.displayFeature("prefs.display_services_manager", .displayServicesManager)) + .addView(when: Valet.installed, vc.displayFeature("prefs.display_valet_integration", .displayValetIntegration)) + .addView(when: true, vc.displayFeature("prefs.display_php_config_finder", .displayPhpConfigFinder)) + .addView(when: true, vc.displayFeature("prefs.display_composer_toolkit", .displayComposerToolkit)) + .addView(when: true, vc.displayFeature("prefs.display_limits_widget", .displayLimitsWidget)) + .addView(when: true, vc.displayFeature("prefs.display_extensions", .displayExtensions)) + .addView(when: true, vc.displayFeature("prefs.display_presets", .displayPresets)) + .addView(when: true, vc.displayFeature("prefs.display_misc", .displayMisc)) + + return vc + } + // swiftlint:enable line_length +} + +class NotificationPreferencesVC: GenericPreferenceVC { + + public static func fromStoryboard() -> GenericPreferenceVC { + let vc = NSStoryboard(name: "Main", bundle: nil) + .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC + + _ = vc.addView(when: true, vc.getNotifyAboutVersionChangePV()) + .addView(when: true, vc.getNotifyAboutPresetsPV()) + .addView(when: Valet.installed, vc.getNotifyAboutSecureTogglePV()) + .addView(when: true, vc.getNotifyAboutGlobalComposerStatusPV()) + .addView(when: true, vc.getNotifyAboutServicesPV()) + .addView(when: Valet.installed, vc.getNotifyAboutPhpFpmChangePV()) + .addView(when: Valet.installed, vc.getWarnAboutNonStandardTldPV()) + + return vc + } + +} diff --git a/phpmon/Domain/Preferences/PrefsVC.swift b/phpmon/Domain/Preferences/PreferencesVC.swift similarity index 70% rename from phpmon/Domain/Preferences/PrefsVC.swift rename to phpmon/Domain/Preferences/PreferencesVC.swift index 8091174..8b594fd 100644 --- a/phpmon/Domain/Preferences/PrefsVC.swift +++ b/phpmon/Domain/Preferences/PreferencesVC.swift @@ -1,5 +1,5 @@ // -// PrefsVC.swift +// PreferencesVC.swift // PHP Monitor // // Created by Nico Verbruggen on 30/03/2021. @@ -28,6 +28,14 @@ class GenericPreferenceVC: NSViewController { Log.perf("deinit: \(String(describing: self)).\(#function)") } + func addView(when condition: Bool, _ view: NSView) -> GenericPreferenceVC { + if condition { + self.views.append(view) + } + + return self + } + func getDynamicIconPV() -> NSView { return CheckboxPreferenceView.make( sectionText: "prefs.dynamic_icon".localized, @@ -66,7 +74,7 @@ class GenericPreferenceVC: NSViewController { ) } - func getAutoRestartPV() -> NSView { + func getAutoRestartServicesPV() -> NSView { return CheckboxPreferenceView.make( sectionText: "prefs.services".localized, descriptionText: "prefs.auto_restart_services_desc".localized, @@ -188,7 +196,7 @@ class GenericPreferenceVC: NSViewController { ) } - func getWarnAboutNonStandardTLD() -> NSView { + func getWarnAboutNonStandardTldPV() -> NSView { return CheckboxPreferenceView.make( sectionText: "prefs.warnings".localized, descriptionText: "prefs.warn_about_non_standard_tld_desc".localized, @@ -198,7 +206,7 @@ class GenericPreferenceVC: NSViewController { ) } - func getDisplayMenuSectionPV( + func displayFeature( _ localizationKey: String, _ preference: PreferenceName, _ first: Bool = false @@ -225,88 +233,3 @@ class GenericPreferenceVC: NSViewController { } } } - -class GeneralPreferencesVC: GenericPreferenceVC { - - // MARK: - Lifecycle - - public static func fromStoryboard() -> GenericPreferenceVC { - let vc = NSStoryboard(name: "Main", bundle: nil) - .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC - - vc.views = [ - vc.getShowPhpDoctorSuggestionsPV(), - vc.getAutoRestartPV(), - vc.getAutomaticComposerUpdatePV(), - vc.getShortcutPV(), - vc.getIntegrationsPV(), - vc.getAutomaticUpdateCheckPV() - ] - - if #available(macOS 13, *) { - vc.views.append(CheckboxPreferenceView.makeLoginItemView()) - } - - return vc - } -} - -class NotificationPreferencesVC: GenericPreferenceVC { - - public static func fromStoryboard() -> GenericPreferenceVC { - let vc = NSStoryboard(name: "Main", bundle: nil) - .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC - - vc.views = [ - vc.getNotifyAboutVersionChangePV(), - vc.getNotifyAboutPresetsPV(), - vc.getNotifyAboutSecureTogglePV(), - vc.getNotifyAboutGlobalComposerStatusPV(), - vc.getNotifyAboutServicesPV(), - vc.getNotifyAboutPhpFpmChangePV(), - vc.getWarnAboutNonStandardTLD() - ] - - return vc - } - -} - -class MenuStructurePreferencesVC: GenericPreferenceVC { - - public static func fromStoryboard() -> GenericPreferenceVC { - let vc = NSStoryboard(name: "Main", bundle: nil) - .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC - - vc.views = [ - vc.getDisplayMenuSectionPV("prefs.display_global_version_switcher", .displayGlobalVersionSwitcher, true), - vc.getDisplayMenuSectionPV("prefs.display_services_manager", .displayServicesManager), - vc.getDisplayMenuSectionPV("prefs.display_valet_integration", .displayValetIntegration), - vc.getDisplayMenuSectionPV("prefs.display_php_config_finder", .displayPhpConfigFinder), - vc.getDisplayMenuSectionPV("prefs.display_composer_toolkit", .displayComposerToolkit), - vc.getDisplayMenuSectionPV("prefs.display_limits_widget", .displayLimitsWidget), - vc.getDisplayMenuSectionPV("prefs.display_extensions", .displayExtensions), - vc.getDisplayMenuSectionPV("prefs.display_presets", .displayPresets), - vc.getDisplayMenuSectionPV("prefs.display_misc", .displayMisc) - - ] - - return vc - } -} - -class AppearancePreferencesVC: GenericPreferenceVC { - - public static func fromStoryboard() -> GenericPreferenceVC { - let vc = NSStoryboard(name: "Main", bundle: nil) - .instantiateController(withIdentifier: "preferencesTemplateVC") as! GenericPreferenceVC - - vc.views = [ - vc.getDynamicIconPV(), - vc.getIconOptionsPV(), - vc.getIconDensityPV() - ] - - return vc - } -} diff --git a/phpmon/Domain/Preferences/Stats.swift b/phpmon/Domain/Preferences/Stats.swift index defb163..1af2aed 100644 --- a/phpmon/Domain/Preferences/Stats.swift +++ b/phpmon/Domain/Preferences/Stats.swift @@ -101,11 +101,11 @@ class Stats { */ public static func evaluateSponsorMessageShouldBeDisplayed() { - if Homebrew.fake { - return Log.info("A fake environment is in use, skipping sponsor alert.") + if Shell is TestableShell { + return Log.info("A fake shell is in use, skipping sponsor alert.") } - if Bundle.main.bundleIdentifier?.contains("beta") ?? false { + if App.identifier.contains(".dev") || App.identifier.contains(".eap") { return Log.info("Sponsor messages never apply to beta builds.") } @@ -140,52 +140,4 @@ class Stats { UserDefaults.standard.set(true, forKey: InternalStats.didSeeSponsorEncouragement.rawValue) } } - - public static func evaluateLastLinkedPhpVersion() { - let currentVersion = PhpEnv.phpInstall.version?.short ?? "" - let previousVersion = Stats.lastGlobalPhpVersion - - if currentVersion == "" { - return Log.warn(" PHP Guard is unable to determine the current PHP version!") - } - Log.info(" The currently linked version of PHP is: \(currentVersion).") - - if previousVersion == "" { - Stats.persistCurrentGlobalPhpVersion(version: currentVersion) - return Log.warn(" PHP Guard is saving the currently linked PHP version (first time only).") - } - Log.info(" Previously, the globally linked PHP version was: \(previousVersion).") - - if previousVersion == currentVersion { - return Log.info(" PHP Guard did not notice any changes in the linked PHP version.") - } - - // At this point, the version is *not* a match - Log.info(" PHP Guard noticed a different PHP version. An alert will be displayed!") - - Task { @MainActor in - BetterAlert() - .withInformation( - title: "startup.version_mismatch.title".localized, - subtitle: "startup.version_mismatch.subtitle".localized( - currentVersion, - previousVersion - ), - description: "startup.version_mismatch.desc".localized() - ) - .withPrimary(text: "startup.version_mismatch.button_switch_back".localized( - previousVersion - ), action: { alert in - alert.close(with: .OK) - Task { MainMenu.shared.switchToAnyPhpVersion(previousVersion) } - }) - .withTertiary(text: "startup.version_mismatch.button_stay".localized( - currentVersion - ), action: { alert in - Stats.persistCurrentGlobalPhpVersion(version: currentVersion) - alert.close(with: .OK) - }) - .show() - } - } } diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift index 2f8cc60..3233267 100644 --- a/phpmon/Domain/Presets/Preset.swift +++ b/phpmon/Domain/Presets/Preset.swift @@ -88,10 +88,14 @@ struct Preset: Codable, Equatable { applyConfigurationValue(key: conf.key, value: conf.value ?? "") } + guard let install = PhpEnvironments.phpInstall else { + Log.info("Cannot toggle extensions if no PHP version is linked.") + return + } + // Apply the extension changes in-place afterward for ext in extensions { - for foundExt in PhpEnv.phpInstall.extensions - where foundExt.name == ext.key && foundExt.enabled != ext.value { + for foundExt in install.extensions where foundExt.name == ext.key && foundExt.enabled != ext.value { Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") await foundExt.toggle() break @@ -125,12 +129,12 @@ struct Preset: Codable, Equatable { // MARK: - Apply Functionality private func switchToPhpVersionIfValid() async -> Bool { - if PhpEnv.shared.currentInstall.version.short == self.version! { + if PhpEnvironments.shared.currentInstall?.version.short == self.version! { Log.info("The version we are supposed to switch to is already active.") return true } - if PhpEnv.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil { + if PhpEnvironments.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil { await MainMenu.shared.switchToPhp(self.version!) return true } else { @@ -140,7 +144,7 @@ struct Preset: Codable, Equatable { subtitle: "alert.php_switch_unavailable.subtitle".localized(version!), description: "alert.php_switch_unavailable.info".localized( version!, - PhpEnv.shared.availablePhpVersions.joined(separator: ", ") + PhpEnvironments.shared.availablePhpVersions.joined(separator: ", ") ) ).withPrimary( text: "alert.php_switch_unavailable.ok".localized @@ -151,7 +155,7 @@ struct Preset: Codable, Equatable { } private func applyConfigurationValue(key: String, value: String) { - guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + guard let file = PhpEnvironments.shared.getConfigFile(forKey: key) else { return } @@ -192,6 +196,7 @@ struct Preset: Codable, Equatable { + info + "" + "" } + // swiftlint:enable void_function_in_ternary // MARK: - Reverting @@ -213,8 +218,12 @@ struct Preset: Codable, Equatable { return nil } - if PhpEnv.shared.currentInstall.version.short != version { - return PhpEnv.shared.currentInstall.version.short + guard let install = PhpEnvironments.phpInstall else { + return nil + } + + if install.version.short != version { + return install.version.short } else { return nil } @@ -226,8 +235,12 @@ struct Preset: Codable, Equatable { private func diffExtensions() -> [String: Bool] { var items: [String: Bool] = [:] + guard let install = PhpEnvironments.phpInstall else { + fatalError("If no PHP version is linked, diffing extensions is not possible.") + } + for (key, value) in self.extensions { - for foundExt in PhpEnv.phpInstall.extensions + for foundExt in install.extensions where foundExt.name == key && foundExt.enabled != value { // Save the original value of the extension items[foundExt.name] = foundExt.enabled @@ -244,7 +257,7 @@ struct Preset: Codable, Equatable { var items: [String: String?] = [:] for (key, _) in self.configuration { - guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + guard let file = PhpEnvironments.shared.getConfigFile(forKey: key) else { break } diff --git a/phpmon/Domain/Progress/ProgressWindow.storyboard b/phpmon/Domain/Progress/ProgressWindow.storyboard index 0d9106b..67ff33f 100644 --- a/phpmon/Domain/Progress/ProgressWindow.storyboard +++ b/phpmon/Domain/Progress/ProgressWindow.storyboard @@ -1,7 +1,7 @@ - + - + @@ -42,7 +42,7 @@ - + diff --git a/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift b/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift index d478bed..8475cff 100644 --- a/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift +++ b/phpmon/Domain/SwiftUI/Domains/VersionPopoverView.swift @@ -97,7 +97,7 @@ struct VersionPopoverView: View { if site.isolatedPhpVersion != nil { information += "alert.composer_php_isolated.desc".localized( site.isolatedPhpVersion!.versionNumber.short, - PhpEnv.phpInstall.version.short + PhpEnvironments.phpInstall?.version.short ?? "???" ) information += "\n\n" } diff --git a/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift b/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift index 7cb5fe5..740d23b 100644 --- a/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift +++ b/phpmon/Domain/SwiftUI/Onboarding/OnboardingView.swift @@ -12,30 +12,36 @@ struct OnboardingTextItem: View { @State var icon: String @State var title: String @State var description: String + @State var unavailable: Bool = false var body: some View { - HStack(alignment: .top, spacing: 5) { - Image(systemName: icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 24, height: 24) - .foregroundColor(Color.appPrimary) - .padding(.trailing, 10) - VStack(alignment: .leading, spacing: 4) { - Text(title.localizedForSwiftUI) - .font(.system(size: 14)) - .lineLimit(3) - Text(description.localizedForSwiftUI) - .foregroundColor(Color.secondary) - .font(.system(size: 13)) - .lineLimit(4) - .fixedSize(horizontal: false, vertical: true) - .frame(minWidth: 0, maxWidth: 800, alignment: .leading) + ZStack { + HStack(alignment: .top, spacing: 5) { + Image(systemName: icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + .foregroundColor(unavailable ? .gray : Color.appPrimary) + .padding(.trailing, 10) + .opacity(self.unavailable ? 0.2 : 1) + VStack(alignment: .leading, spacing: 4) { + Text(title.localizedForSwiftUI) + .font(.system(size: 14)) + .lineLimit(3) + .opacity(self.unavailable ? 0.5 : 1) + Text(description.localizedForSwiftUI) + .foregroundColor(Color.secondary) + .font(.system(size: 13)) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + .frame(minWidth: 0, maxWidth: 800, alignment: .leading) + .opacity(self.unavailable ? 0.5 : 1) + } } + .padding() + .overlay(RoundedRectangle(cornerRadius: 5) + .stroke(Color.gray.opacity(self.unavailable ? 0.1 : 0.3), lineWidth: 1)) } - .padding() - .overlay(RoundedRectangle(cornerRadius: 5) - .stroke(Color.gray.opacity(0.3), lineWidth: 1)) } } @@ -53,9 +59,15 @@ struct OnboardingView: View { .font(.title) .bold() .padding(.bottom, 5) - Text("onboarding.explore".localized) - .padding(.bottom) - .padding(.trailing) + .padding(.top, 8) + .foregroundColor(Color.appPrimary) + Text( + Valet.installed + ? "onboarding.explore".localizedForSwiftUI + : "onboarding.explore.lite".localizedForSwiftUI + ) + .padding(.bottom) + .padding(.trailing) } .padding(.top, 10) } @@ -72,17 +84,20 @@ struct OnboardingView: View { OnboardingTextItem( icon: "checkmark.circle.fill", title: "onboarding.tour.services.title", - description: "onboarding.tour.services" + description: "onboarding.tour.services", + unavailable: !Valet.installed ) OnboardingTextItem( icon: "list.bullet.circle.fill", title: "onboarding.tour.domains.title", - description: "onboarding.tour.domains" + description: "onboarding.tour.domains", + unavailable: !Valet.installed ) OnboardingTextItem( icon: "pin.circle.fill", title: "onboarding.tour.isolation.title", - description: "onboarding.tour.isolation" + description: "onboarding.tour.isolation", + unavailable: !Valet.installed ) } }.padding() @@ -111,6 +126,8 @@ struct OnboardingView: View { Button("onboarding.tour.close".localized) { App.shared.onboardingWindowController?.close() } + .padding(.bottom, 5) + .padding(.top, 10) } } .padding(.leading) @@ -123,7 +140,6 @@ struct OnboardingView_Previews: PreviewProvider { static var previews: some View { Group { OnboardingView() - OnboardingView().preferredColorScheme(.dark) } } } diff --git a/phpmon/Domain/SwiftUI/PhpManager/BlockingOverlayView.swift b/phpmon/Domain/SwiftUI/PhpManager/BlockingOverlayView.swift new file mode 100644 index 0000000..27c6762 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/BlockingOverlayView.swift @@ -0,0 +1,62 @@ +// +// BlockingOverlayView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 19/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import SwiftUI + +struct BlockingOverlayView: View { + var isBlocking: Bool + var titleText: String + var detailText: String + var content: () -> Content + + init( + busy: Bool, + title: String, + text: String, + @ViewBuilder content: @escaping () -> Content + ) { + self.isBlocking = busy + self.titleText = title + self.detailText = text + self.content = content + } + + var body: some View { + ZStack(alignment: .center) { + content().opacity(isBlocking ? 0.2 : 1) + if isBlocking { + VStack { + ActivityIndicator() + Text(titleText) + .font(.system(size: 14)) + .bold() + .foregroundColor(.primary) + .padding(.top, 8) + Text(detailText) + .font(.system(size: 11)) + .foregroundColor(.primary) + .padding(.top, -4) + }.padding(60) + } + }.background(Color.white) + .disabled(isBlocking) + } +} + +struct ActivityIndicator: NSViewRepresentable { + func makeNSView(context: Context) -> NSProgressIndicator { + let nsView = NSProgressIndicator() + nsView.style = .spinning + nsView.startAnimation(nil) + return nsView + } + + func updateNSView(_ nsView: NSProgressIndicator, context: Context) { + } +} diff --git a/phpmon/Domain/SwiftUI/PhpManager/BrewFormulaUI.swift b/phpmon/Domain/SwiftUI/PhpManager/BrewFormulaUI.swift new file mode 100644 index 0000000..907ad86 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/BrewFormulaUI.swift @@ -0,0 +1,30 @@ +// +// BrewFormulaUI.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 02/05/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import SwiftUI + +extension BrewFormula { + var icon: String { + if self.hasUpgrade { + return "arrow.up.square.fill" + } else if self.isInstalled { + return "checkmark.square.fill" + } + return "square.dashed" + } + + var iconColor: Color { + if self.hasUpgrade { + return Color("StatusColorBlue") + } else if self.isInstalled { + return Color("StatusColorGreen") + } + return Color.gray.opacity(0.3) + } +} diff --git a/phpmon/Domain/SwiftUI/PhpManager/FakeBrewFormulaeHandler.swift b/phpmon/Domain/SwiftUI/PhpManager/FakeBrewFormulaeHandler.swift new file mode 100644 index 0000000..759fdd1 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/FakeBrewFormulaeHandler.swift @@ -0,0 +1,58 @@ +// +// FakeBrewFormulaeHandler.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 27/05/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class FakeBrewFormulaeHandler: HandlesBrewFormulae { + public func loadPhpVersions(loadOutdated: Bool) async -> [BrewFormula] { + return [ + BrewFormula( + name: "php", + displayName: "PHP 8.2", + installedVersion: "8.2.3", + upgradeVersion: "8.2.4" + ), + BrewFormula( + name: "php@8.1", + displayName: "PHP 8.1", + installedVersion: "8.1.17", + upgradeVersion: nil + ), + BrewFormula( + name: "php@8.0", + displayName: "PHP 8.0", + installedVersion: nil, + upgradeVersion: nil + ), + BrewFormula( + name: "php@7.4", + displayName: "PHP 7.4", + installedVersion: nil, + upgradeVersion: nil + ), + BrewFormula( + name: "php@7.3", + displayName: "PHP 7.3", + installedVersion: nil, + upgradeVersion: nil + ), + BrewFormula( + name: "php@7.2", + displayName: "PHP 7.2", + installedVersion: nil, + upgradeVersion: nil + ), + BrewFormula( + name: "php@7.1", + displayName: "PHP 7.1", + installedVersion: nil, + upgradeVersion: nil + ) + ] + } +} diff --git a/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeStatus.swift b/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeStatus.swift new file mode 100644 index 0000000..b02f2b2 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeStatus.swift @@ -0,0 +1,21 @@ +// +// PhpFormulaeStatus.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 02/05/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class PhpFormulaeStatus: ObservableObject { + @Published var busy: Bool + @Published var title: String + @Published var description: String + + init(busy: Bool, title: String, description: String) { + self.busy = busy + self.title = title + self.description = description + } +} diff --git a/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeView.swift b/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeView.swift new file mode 100644 index 0000000..83c8ffc --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/PhpFormulaeView.swift @@ -0,0 +1,360 @@ +// +// PhpFormulaeView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 17/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import SwiftUI + +// swiftlint:disable type_body_length +struct PhpFormulaeView: View { + @ObservedObject var formulae: BrewFormulaeObservable + @ObservedObject var status: PhpFormulaeStatus + var handler: HandlesBrewFormulae + + init( + formulae: BrewFormulaeObservable, + handler: HandlesBrewFormulae + ) { + self.formulae = formulae + self.handler = handler + + self.status = PhpFormulaeStatus( + busy: true, + title: "phpman.busy.title".localized, + description: "phpman.busy.description.outdated".localized + ) + + Task { [self] in + await self.initialLoad() + } + } + + private func initialLoad() async { + guard let version = Brew.shared.version else { + return + } + + await delay(seconds: 1) + + if version.major != 4 { + Task { @MainActor in + self.presentErrorAlert( + title: "phpman.warnings.unsupported.title".localized, + description: "phpman.warnings.unsupported.desc".localized(version.text), + button: "generic.ok".localized, + style: .warning + ) + } + } + + await PhpEnvironments.detectPhpVersions() + await self.handler.refreshPhpVersions(loadOutdated: false) + await self.handler.refreshPhpVersions(loadOutdated: true) + self.status.busy = false + } + + private func reload() async { + Task { @MainActor in + self.status.busy = true + self.status.title = "phpman.busy.title".localized + self.status.description = "phpman.busy.description.outdated".localized + } + await self.handler.refreshPhpVersions(loadOutdated: true) + Task { @MainActor in + self.status.busy = false + } + } + + var body: some View { + VStack { + HStack(alignment: .center, spacing: 15) { + Image(systemName: "arrow.down.to.line.circle.fill") + .resizable() + .frame(width: 40, height: 40) + .foregroundColor(Color.blue) + .padding(12) + VStack(alignment: .leading, spacing: 5) { + Text("phpman.description".localizedForSwiftUI) + .font(.system(size: 12)) + .frame(maxWidth: .infinity, alignment: .leading) + Text("phpman.disclaimer".localizedForSwiftUI) + .font(.system(size: 12)) + .foregroundColor(.gray) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(10) + + if self.hasUpdates { + Divider() + HStack(alignment: .center, spacing: 15) { + Text("phpman.has_updates.description".localizedForSwiftUI) + .foregroundColor(.gray) + .font(.system(size: 11)) + + Button("phpman.has_updates.button".localizedForSwiftUI, action: { + Task { await self.upgradeAll(self.formulae.upgradeable) } + + }) + .focusable(false) + .disabled(self.status.busy) + } + .padding(10) + } else { + Divider() + + HStack(alignment: .center, spacing: 15) { + Button { + Task { await self.reload() } + } label: { + Image(systemName: "arrow.clockwise") + .buttonStyle(.automatic) + .controlSize(.large) + } + .focusable(false) + .disabled(self.status.busy) + + Text("phpman.refresh.button.description".localizedForSwiftUI) + .foregroundColor(.gray) + .font(.system(size: 11)) + } + .padding(10) + } + + BlockingOverlayView(busy: self.status.busy, title: self.status.title, text: self.status.description) { + List(Array(formulae.phpVersions.enumerated()), id: \.1.name) { (index, formula) in + HStack { + Image(systemName: formula.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 16, height: 16) + .foregroundColor(formula.iconColor) + .padding(.horizontal, 5) + VStack(alignment: .leading, spacing: 2) { + Text(formula.displayName).bold() + + if formula.isInstalled && formula.hasUpgrade { + Text("phpman.version.has_update".localized( + formula.installedVersion!, + formula.upgradeVersion! + )) + .font(.system(size: 11)) + .foregroundColor(.gray) + } else if formula.isInstalled && formula.installedVersion != nil { + Text("phpman.version.installed".localized(formula.installedVersion!)) + .font(.system(size: 11)) + .foregroundColor(.gray) + } else { + Text("phpman.version.available_for_installation".localizedForSwiftUI) + .font(.system(size: 11)) + .foregroundColor(.gray) + } + + if !formula.healthy { + Text("phpman.version.broken".localizedForSwiftUI) + .font(.system(size: 11)) + .foregroundColor(.red) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + if !formula.healthy { + Button("phpman.buttons.repair".localizedForSwiftUI, role: .destructive) { + Task { await self.repairAll() } + } + } + + if formula.isInstalled { + Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) { + Task { await self.confirmUninstall(formula) } + } + } else { + Button("phpman.buttons.install".localizedForSwiftUI) { + Task { await self.install(formula) } + } + } + } + .listRowBackground(index % 2 == 0 + ? Color.gray.opacity(0) + : Color.gray.opacity(0.08) + ) + .padding(.vertical, 10) + } + } + }.frame(width: 600, height: 600) + } + + public func runCommand(_ command: InstallAndUpgradeCommand) async { + if PhpEnvironments.shared.isBusy { + self.presentErrorAlert( + title: "phpman.action_prevented_busy.title".localized, + description: "phpman.action_prevented_busy.desc".localized, + button: "generic.ok".localized + ) + return + } + + do { + self.setBusyStatus(true) + try await command.execute { progress in + Task { @MainActor in + self.status.title = progress.title + self.status.description = progress.description + self.status.busy = progress.value != 1 + + // Whenever a key step is finished, refresh the PHP versions + if progress.value == 1 { + await self.handler.refreshPhpVersions(loadOutdated: false) + } + } + } + // Finally, after completing the command, also refresh PHP versions + await self.handler.refreshPhpVersions(loadOutdated: false) + // and mark the app as no longer busy + self.setBusyStatus(false) + } catch let error { + let error = error as! BrewCommandError + let messages = error.log.suffix(2).joined(separator: "\n") + + self.setBusyStatus(false) + await self.handler.refreshPhpVersions(loadOutdated: false) + + self.presentErrorAlert( + title: "phpman.failures.install.title".localized, + description: "phpman.failures.install.desc".localized(messages), + button: "generic.ok".localized + ) + } + } + + public func repairAll() async { + await self.runCommand(InstallAndUpgradeCommand( + title: "phpman.operations.repairing".localized, + upgrading: [], + installing: [] + )) + } + + public func upgradeAll(_ formulae: [BrewFormula]) async { + await self.runCommand(InstallAndUpgradeCommand( + title: "phpman.operations.updating".localized, + upgrading: formulae, + installing: [] + )) + } + + public func install(_ formula: BrewFormula) async { + await self.runCommand(InstallAndUpgradeCommand( + title: "phpman.operations.installing".localized(formula.displayName), + upgrading: [], + installing: [formula] + )) + } + + public func confirmUninstall(_ formula: BrewFormula) async { + // Disallow removal of the currently active versipn + if formula.installedVersion == PhpEnvironments.shared.currentInstall?.version.text { + self.presentErrorAlert( + title: "phpman.uninstall_prevented.title".localized, + description: "phpman.uninstall_prevented.desc".localized, + button: "generic.ok".localized + ) + return + } + + Alert.confirm( + onWindow: App.shared.versionManagerWindowController!.window!, + messageText: "phpman.warnings.removal.title".localized(formula.displayName), + informativeText: "phpman.warnings.removal.desc".localized(formula.displayName), + buttonTitle: "phpman.warnings.removal.button".localized, + buttonIsDestructive: true, + secondButtonTitle: "generic.cancel".localized, + style: .warning, + onFirstButtonPressed: { + Task { await self.uninstall(formula) } + } + ) + } + + public func uninstall(_ formula: BrewFormula) async { + let command = RemovePhpVersionCommand(formula: formula.name) + + do { + self.setBusyStatus(true) + try await command.execute { progress in + Task { @MainActor in + self.status.title = progress.title + self.status.description = progress.description + self.status.busy = progress.value != 1 + + if progress.value == 1 { + await self.handler.refreshPhpVersions(loadOutdated: false) + self.setBusyStatus(false) + } + } + } + } catch { + self.setBusyStatus(false) + self.presentErrorAlert( + title: "phpman.failures.uninstall.title".localized, + description: "phpman.failures.uninstall.desc".localized( + "brew uninstall \(formula.name) --force" + ), + button: "generic.ok".localized + ) + } + } + + public func setBusyStatus(_ busy: Bool) { + PhpEnvironments.shared.isBusy = busy + if busy { + Task { @MainActor in + MainMenu.shared.setBusyImage() + MainMenu.shared.rebuild() + self.status.busy = busy + } + } else { + Task { @MainActor in + MainMenu.shared.updatePhpVersionInStatusBar() + self.status.busy = busy + } + } + } + + public func presentErrorAlert( + title: String, + description: String, + button: String, + style: NSAlert.Style = .critical + ) { + Alert.confirm( + onWindow: App.shared.versionManagerWindowController!.window!, + messageText: title, + informativeText: description, + buttonTitle: button, + secondButtonTitle: "", + style: style, + onFirstButtonPressed: {} + ) + } + + var hasUpdates: Bool { + return self.formulae.phpVersions.contains { formula in + return formula.hasUpgrade + } + } +} +// swiftlint:enable type_body_length + +struct PhpFormulaeView_Previews: PreviewProvider { + static var previews: some View { + PhpFormulaeView( + formulae: Brew.shared.formulae, + handler: FakeBrewFormulaeHandler() + ).frame(width: 600, height: 600) + } +} diff --git a/phpmon/Domain/SwiftUI/PhpManager/PhpVersionManagerWC.swift b/phpmon/Domain/SwiftUI/PhpManager/PhpVersionManagerWC.swift new file mode 100644 index 0000000..e786a30 --- /dev/null +++ b/phpmon/Domain/SwiftUI/PhpManager/PhpVersionManagerWC.swift @@ -0,0 +1,52 @@ +// +// PhpVersionManagerWindowController.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 19/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import Cocoa +import SwiftUI + +class PhpVersionManagerWindowController: PMWindowController { + + // MARK: - Window Identifier + + var view: PhpFormulaeView! + + override var windowName: String { + return "PhpFormulaeView" + } + + public static func create(delegate: NSWindowDelegate?) { + let windowController = Self() + windowController.window = NSWindow() + windowController.view = PhpFormulaeView( + formulae: Brew.shared.formulae, + handler: BrewFormulaeHandler() + ) + + guard let window = windowController.window else { return } + window.title = "" + window.styleMask = [.titled, .closable, .miniaturizable] + window.titlebarAppearsTransparent = true + window.delegate = delegate ?? windowController + window.contentView = NSHostingView(rootView: windowController.view) + window.setContentSize(NSSize(width: 600, height: 800)) + + App.shared.versionManagerWindowController = windowController + } + + public static func show(delegate: NSWindowDelegate? = nil) { + if App.shared.versionManagerWindowController == nil { + Self.create(delegate: delegate) + } + + App.shared.versionManagerWindowController?.showWindow(self) + App.shared.versionManagerWindowController?.positionWindowInTopLeftCorner() + + NSApp.activate(ignoringOtherApps: true) + } +} diff --git a/phpmon/Domain/SwiftUI/Progress/ProgressViewSubject.swift b/phpmon/Domain/SwiftUI/Progress/ProgressViewSubject.swift new file mode 100644 index 0000000..b739aad --- /dev/null +++ b/phpmon/Domain/SwiftUI/Progress/ProgressViewSubject.swift @@ -0,0 +1,22 @@ +// +// ProgressViewSubject.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 11/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation +import SwiftUI + +class ProgressViewSubject: ObservableObject { + @Published var title: String + @Published var description: String? + @Published var progress: Double + + init(title: String, description: String) { + self.title = title + self.description = description + self.progress = 0 + } +} diff --git a/phpmon/Domain/SwiftUI/Progress/ProgressWindowView.swift b/phpmon/Domain/SwiftUI/Progress/ProgressWindowView.swift new file mode 100644 index 0000000..d6edc7a --- /dev/null +++ b/phpmon/Domain/SwiftUI/Progress/ProgressWindowView.swift @@ -0,0 +1,65 @@ +// +// ProgressWindowView.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 11/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import SwiftUI + +struct ProgressWindowView: View { + @ObservedObject var subject: ProgressViewSubject + + var body: some View { + VStack(alignment: .leading) { + VStack(alignment: .leading) { + Text(subject.title) + .font(.system(size: 14)) + .bold() + if subject.description != nil { + Text(subject.description!) + .font(.system(size: 13)) + } + } + .padding(.leading, 20) + .padding(.top, 12) + ProgressView(value: subject.progress) + .padding(.top, 0) + .padding(.bottom, 12) + .padding(.horizontal, 20) + } + } + + @MainActor static func display(_ subject: ProgressViewSubject) async -> NSWindowController { + let view = ProgressWindowView(subject: subject) + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 420, height: 240), + styleMask: [.titled, .closable, .utilityWindow], + backing: .buffered, + defer: false + ) + + window.title = "" + window.titlebarAppearsTransparent = true + window.contentView = NSHostingView(rootView: view) + let controller = NSWindowController(window: window) + controller.showWindow(nil) + controller.positionWindowInTopLeftCorner() + controller.window?.makeKeyAndOrderFront(self) + // NSApp.activate(ignoringOtherApps: true) + return controller + } +} + +struct ProgressWindowView_Previews: PreviewProvider { + static var previews: some View { + ProgressWindowView( + subject: ProgressViewSubject( + title: "Long running task", + description: "Please be patient" + ) + ) + } +} diff --git a/phpmon/Domain/SwiftUI/Warning/WarningListView.swift b/phpmon/Domain/SwiftUI/Warning/WarningListView.swift index 7ccd9e8..b664d8c 100644 --- a/phpmon/Domain/SwiftUI/Warning/WarningListView.swift +++ b/phpmon/Domain/SwiftUI/Warning/WarningListView.swift @@ -11,12 +11,22 @@ import SwiftUI struct WarningListView: View { @ObservedObject var warningManager: WarningManager - init(empty: Bool = false) { - if empty { - WarningManager.shared.warnings = [] + init(empty: Bool = false, fake: Bool = false, manager: WarningManager? = nil) { + if manager == nil { + // Use the singleton by default + warningManager = WarningManager.shared + } else { + // Use a provided instance (for e.g. preview purposes) + warningManager = manager! } - warningManager = WarningManager.shared + if fake { + warningManager.warnings = warningManager.evaluations + } + + if empty { + warningManager.clearWarnings() + } } var body: some View { @@ -41,10 +51,14 @@ struct WarningListView: View { Divider() HStack(alignment: .center, spacing: 15) { - Button("warnings.refresh.button".localizedForSwiftUI) { + Button { Task { // Reload warnings - await WarningManager.shared.checkEnvironment() + await self.warningManager.checkEnvironment() } + } label: { + Image(systemName: "arrow.clockwise") + .buttonStyle(.automatic) + .controlSize(.large) } Text("warnings.refresh.button.description".localizedForSwiftUI) .foregroundColor(.gray) @@ -54,7 +68,7 @@ struct WarningListView: View { List { VStack(alignment: .leading, spacing: 0) { - if warningManager.warnings.isEmpty { + if !warningManager.hasWarnings() { NoWarningsView() } else { ForEach(warningManager.warnings) { warning in @@ -82,11 +96,11 @@ struct WarningListView: View { struct WarningListView_Previews: PreviewProvider { static var previews: some View { - WarningListView(empty: true) + WarningListView(empty: true, fake: true, manager: WarningManager()) .frame(width: 600, height: 480) .previewDisplayName("Empty List") - WarningListView(empty: false) + WarningListView(empty: false, fake: true, manager: WarningManager()) .frame(width: 600, height: 480) .previewDisplayName("List With All Warnings") } diff --git a/phpmon/Domain/Warnings/Services/PhpConfigChecker.swift b/phpmon/Domain/Warnings/Services/PhpConfigChecker.swift index 656959f..6ba9e7f 100644 --- a/phpmon/Domain/Warnings/Services/PhpConfigChecker.swift +++ b/phpmon/Domain/Warnings/Services/PhpConfigChecker.swift @@ -8,6 +8,11 @@ import Foundation +struct FileExistenceCheck { + let condition: (() -> Bool)? + let path: String +} + class PhpConfigChecker { public static var shared = PhpConfigChecker() @@ -17,15 +22,21 @@ class PhpConfigChecker { public func check() { missing = [] - let shouldExist = [ - "php.ini", - "php-fpm.conf", - "php-fpm.d/valet-fpm.conf" + let shouldExist: [FileExistenceCheck] = [ + FileExistenceCheck(condition: nil, path: "php.ini"), + FileExistenceCheck(condition: nil, path: "php-fpm.conf"), + FileExistenceCheck(condition: { Valet.installed }, path: "php-fpm.d/valet-fpm.conf") ] - for version in PhpEnv.shared.availablePhpVersions { + for version in PhpEnvironments.shared.availablePhpVersions { for file in shouldExist { - let fullFilePath = Paths.etcPath.appending("/php/\(version)/\(file)") + // Early exit in case our condition is not met + if file.condition != nil && file.condition!() == false { + continue + } + + // Do the check + let fullFilePath = Paths.etcPath.appending("/php/\(version)/\(file.path)") if !FileSystem.fileExists(fullFilePath) { missing.append(fullFilePath) } diff --git a/phpmon/Domain/Warnings/WarningManager.swift b/phpmon/Domain/Warnings/WarningManager.swift index da9d8fd..af30420 100644 --- a/phpmon/Domain/Warnings/WarningManager.swift +++ b/phpmon/Domain/Warnings/WarningManager.swift @@ -10,11 +10,19 @@ import Foundation import Cocoa class WarningManager: ObservableObject { - static var shared: WarningManager = WarningManager() + /// These warnings are the ones that are ready to be displayed. + @Published public var warnings: [Warning] = [] + + /// This variable is thread-safe and may be modified at any time. + /// When all temporary warnings are set, you may broadcast these changes + /// and they will be sent to the @Published variable via the main thread. + private var temporaryWarnings: [Warning] = [] + init() { if isRunningSwiftUIPreview { + /// SwiftUI previews will always list all possible evaluations. self.warnings = self.evaluations } } @@ -60,8 +68,6 @@ class WarningManager: ObservableObject { ) ] - @Published public var warnings: [Warning] = [] - public func hasWarnings() -> Bool { return !warnings.isEmpty } @@ -70,33 +76,41 @@ class WarningManager: ObservableObject { Task { await WarningManager.shared.checkEnvironment() } } + @MainActor func clearWarnings() { + self.warnings = [] + } + + @MainActor func broadcastWarnings() { + self.warnings = temporaryWarnings + } + /** Checks the user's environment and checks if any special warnings apply. */ func checkEnvironment() async { if ProcessInfo.processInfo.environment["EXTREME_DOCTOR_MODE"] != nil { - // For debugging purposes, we may wish to see all possible evaluations listed - Task { @MainActor in - self.warnings = self.evaluations - } - } else { - // Otherwise, loop over the actual evaluations and list the warnings - await loopOverEvaluations() + self.temporaryWarnings = self.evaluations + await self.broadcastWarnings() + return } + await evaluate() await MainMenu.shared.rebuild() } - private func loopOverEvaluations() async { - Task { @MainActor in - self.warnings = [] - } + /** + Runs through all evaluations and appends any applicable warning results. + Will automatically broadcast these warnings. + */ + private func evaluate() async { + self.temporaryWarnings = [] + for check in self.evaluations where await check.applies() { Log.info("[DOCTOR] \(check.name) (!)") - Task { @MainActor in - self.warnings.append(check) - } + self.temporaryWarnings.append(check) continue } + + await self.broadcastWarnings() } } diff --git a/phpmon/Domain/Watcher/App+BrewWatch.swift b/phpmon/Domain/Watcher/App+BrewWatch.swift new file mode 100644 index 0000000..6986b7b --- /dev/null +++ b/phpmon/Domain/Watcher/App+BrewWatch.swift @@ -0,0 +1,40 @@ +// +// App+BrewWatch.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 03/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +extension App { + + public func prepareHomebrewWatchers() { + let notifier = FSNotifier( + for: URL(fileURLWithPath: Paths.binPath), + eventMask: .all, + onChange: { Task { await self.onHomebrewPhpModification() } } + ) + + App.shared.watchers[.homebrewBinaries] = notifier + } + + public func destroyHomebrewWatchers() { + // Removing requires termination and then removing reference + self.watchers[.homebrewBinaries]?.terminate() + self.watchers[.homebrewBinaries] = nil + } + + public func onHomebrewPhpModification() async { + // let previous = PhpEnvironments.shared.currentInstall?.version.text + Log.info("Something changed in the Homebrew binary directory...") + await PhpEnvironments.detectPhpVersions() + await MainMenu.shared.refreshActiveInstallation() + // let new = PhpEnvironments.shared.currentInstall?.version.text + + // TODO: + // Check if the new and previous version are different + // if so, we can show a notification if needed + } +} diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift index e7637f9..9588b80 100644 --- a/phpmon/Domain/Watcher/App+ConfigWatch.swift +++ b/phpmon/Domain/Watcher/App+ConfigWatch.swift @@ -33,7 +33,13 @@ extension App { return } - let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(PhpEnv.phpInstall.version.short)") + guard let install = PhpEnvironments.phpInstall else { + Log.info("It appears as if no PHP installation is currently active.") + Log.info("The FS watcher will be disabled until a PHP install is active.") + return + } + + let url = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(install.version.short)") // Check whether the watcher exists and schedule on the main thread // if we don't consistently do this, the app will create duplicate watchers diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 6ede9f0..45b5d2c 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -17,6 +17,10 @@ "mi_php_broken_2" = "Try running `php -v` in your terminal."; "mi_php_broken_3" = "You could also try switching to another version."; "mi_php_broken_4" = "Running `brew reinstall php` (or for the equivalent version) might help."; +"mi_no_php_linked" = "No PHP version linked!"; +"mi_fix_php_link" = "Fix Automatically..."; +"mi_no_php_linked_explain" = "What's This?"; +"mi_php_version_manager" = "PHP Version Manager..."; "mi_diagnostics" = "Diagnostics"; "mi_active_services" = "Active Services"; @@ -67,6 +71,7 @@ "mi_preferences" = "Preferences..."; "mi_donate" = "Donate..."; "mi_check_for_updates" = "Check for Updates..."; +"mi_lite_mode" = "About Standalone Mode..."; "mi_quit" = "Quit PHP Monitor"; "mi_about" = "About PHP Monitor"; @@ -84,6 +89,74 @@ "mi_xdebug_actions" = "Actions"; "mi_xdebug_disable_all" = "Disable All Modes"; +// PHPMAN + +"phpman.busy.title" = "Checking for updates!"; +"phpman.busy.description.outdated" = "Checking if any PHP version is outdated..."; + +"phpman.version.broken" = "This version appears to be broken, you can attempt repair."; +"phpman.version.has_update" = "Version %@ installed, %@ available."; +"phpman.version.installed" = "Version %@ is currently installed."; +"phpman.version.available_for_installation" = "This version can be installed."; +"phpman.buttons.uninstall" = "Uninstall"; +"phpman.buttons.install" = "Install"; +"phpman.buttons.update" = "Update"; +"phpman.buttons.repair" = "Repair"; + +"phpman.title" = "PHP Version Manager"; +"phpman.description" = "**PHP Version Manager** lets you install, upgrade and delete different PHP versions via Homebrew without needing to run the commands in the terminal yourself."; +"phpman.disclaimer" = "Please note that installing or upgrading PHP versions may cause other Homebrew packages to be upgraded as well. Most installation steps usually take some time, so please be patient while Homebrew does its job."; +"phpman.refresh.button" = "Search for Updates"; +"phpman.refresh.button.description" = "You can press the refresh button to check if any updates are available to installed PHP versions."; + +"phpman.has_updates.description" = "One or more updates are available. (Please note that PHP Monitor will always install or update PHP versions in bulk, so you will always upgrade all installations at once.)"; +"phpman.has_updates.button" = "Upgrade All"; + +"phpman.warnings.unsupported.title" = "Your version of Homebrew may cause issues"; +"phpman.warnings.unsupported.desc" = "No functionality is disabled, but some commands may not work as expected. You are currently running Homebrew %@. + +Currently, Homebrew 4 is the only supported version for the PHP Version Manager. If you are running a newer version of Homebrew, you may wish to check if a newer version of PHP Monitor is available."; + +"phpman.warnings.removal.title" = "Are you sure you want to uninstall %@?"; +"phpman.warnings.removal.desc" = "Please note that configuration files will not be removed, so it should be easy to reinstall later if needed. + +You may be asked for your password during the uninstallation process if file permissions don't allow a simple removal."; +"phpman.warnings.removal.button" = "Uninstall"; + +"phpman.failures.install.title" = "Installation failed!"; +"phpman.failures.install.desc" = "Unfortunately, the operation returned an error code for some reason. You may find that the formulae have been correctly installed or upgraded. Unfortunately, I can't do much about this. Please check out the last few messages from Homebrew here for more information on what happened: + +%@"; + +"phpman.action_prevented_busy.title" = "PHP Monitor is currently busy."; +"phpman.action_prevented_busy.desc" = "PHP Monitor is currently doing something like switching between PHP versions. To ensure your system does not break, you will need to wait until PHP Monitor is ready in order before you try this again."; + +"phpman.uninstall_prevented.title" = "You cannot uninstall the currently active version of PHP via PHP Monitor."; +"phpman.uninstall_prevented.desc" = "In order to prevent issues with PHP Monitor and further crashes, it isn't possible to uninstall the currently linked version of PHP via this UI. You can switch versions and try again, or uninstall this version manually via the terminal.\n\nPlease note that PHP Monitor may crash if you uninstall the currently linked PHP version."; + +"phpman.failures.uninstall.title" = "Uninstall failed!"; +"phpman.failures.uninstall.desc" = "Unfortunately, the automatic uninstallation failed. You can manually try to run this command: `%@` and find out what goes wrong. Remember to restart PHP Monitor (or press the refresh button) when this is done."; + +"phpman.unlinked.title" = "None of the versions of PHP installed on your system are currently linked."; +"phpman.unlinked.desc" = "You likely still have some version of PHP that is currently installed, but currently no version of PHP is linked."; +"phpman.unlinked.detail" = "Without any PHP version linked, the `php` binary is not accessible on your system and you cannot run any PHP scripts without explicitly being part of the PATH. You can have PHP Monitor automatically resolve this problem (choose Fix Automatically in the main menu), or fix it yourself by running `brew link php --force`."; + +"phpman.operations.repairing" = "Repairing installations..."; +"phpman.operations.updating" = "Installing updates..."; +"phpman.operations.installing" = "Installing %@..."; + +"phpman.steps.fetching" = "Fetching..."; +"phpman.steps.downloading" = "Downloading package data..."; +"phpman.steps.installing" = "Installing some package data..."; +"phpman.steps.pouring" = "Pouring... This can take a while..."; +"phpman.steps.summary" = "Some package has finished installing..."; + +// LITE MODE + +"lite_mode_explanation.title" = "You are currently running PHP Monitor in Standalone Mode."; +"lite_mode_explanation.subtitle" = "PHP Monitor has additional features that are available for use if you happen to be a user of Laravel Valet. Right now, PHP Monitor could not detect an active installation of Valet on your system, so those features are unavailable."; +"lite_mode_explanation.description" = "For more information, I'd recommend checking out the README (accessible on GitHub) which will explain what steps you need to take to install Valet and make PHP Monitor run correctly with it installed. You will need to restart PHP Monitor after installing Laravel Valet before it will leave Standalone Mode."; + // GENERIC "generic.ok" = "OK"; @@ -360,7 +433,8 @@ This has no effect on other terminals, only for the particular terminal session "notification.preset_reverted_desc" = "The last preset you applied has been undone. Your previous configuration is now active."; "notification.phpmon_updated.title" = "PHP Monitor has been updated!"; -"notification.phpmon_updated.desc" = "You are now running PHP Monitor v%@."; +"notification.phpmon_updated.desc" = "You are now running PHP Monitor v%@. Thanks for staying up-to-date!"; +"notification.phpmon_updated_dev.desc" = "PHP Monitor v%@ (build %@) is now installed and active."; // Composer Update "alert.composer_missing.title" = "Composer not found!"; @@ -512,6 +586,12 @@ You can do this by running `composer global update` in your terminal. After that "startup.errors.php_opt.subtitle" = "The PHP alias was not found in `%@`. The app will not work correctly until you resolve this issue."; "startup.errors.php_opt.desc" = "If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation."; +/// PHP binary is broken +"startup.errors.dyld_library.title" = "PHP is installed, but appears to be broken"; +"startup.errors.dyld_library.subtitle" = "When PHP Monitor is attempting to run commands, it is failing to do so correctly. This is usually an indicator of a broken PHP installation."; +"startup.errors.dyld_library.desc" = "Running `brew reinstall php && brew link php` in your Terminal may resolve this issue, so please give that a try."; + +/// Valet is not installed "startup.errors.valet_executable.title" = "Laravel Valet is not correctly installed"; "startup.errors.valet_executable.subtitle" = "You must install Valet with Composer. The app will not work correctly until you resolve this issue."; "startup.errors.valet_executable.desc" = "If you haven't installed Laravel Valet yet, please do so first. If you have it installed but are seeing this message anyway, then try running `which valet` in Terminal, it should return: `%@`."; @@ -666,7 +746,7 @@ COMMON TROUBLESHOOTING TIPS • %@ -When files like these are missing, it's recommended to reinstall the appropriate PHP version(s) via Homebrew again, which should restore the configuration files that are missing. Missing configuration files can be the reason why you get '502 Bad Gateway' errors, even after running Fix My Valet."; +When files like these are missing, you should switch to the PHP version associated with those files: that may resolve the problem. If this doesn't fix the issue, it's recommended to reinstall the appropriate PHP version(s) via Homebrew again, which should restore the configuration files that are missing. Missing configuration files can be the reason why you get '502 Bad Gateway' errors, even after running Fix My Valet (if you are using Valet)."; "warnings.none" = "There are no recommendations available for you right now. You're all good!"; @@ -674,15 +754,18 @@ When files like these are missing, it's recommended to reinstall the appropriate "onboarding.title" = "Welcome Tour"; "onboarding.welcome" = "Welcome to PHP Monitor!"; -"onboarding.explore" = "Congrats, you now have access to PHP Monitor's entire suite of functionality. You can learn more about some of the features that PHP Monitor has to offer on this screen."; +"onboarding.explore" = "You now have access to PHP Monitor's entire suite of functionality. You can learn more about some of the features that PHP Monitor has to offer on this screen."; +"onboarding.explore.lite" = "You now have access to PHP Monitor's most important features. +Please note that some features (greyed out below) are currently unavailable because Laravel Valet is not active."; "onboarding.tour.menu_bar.title" = "Power In Your Menu Bar"; -"onboarding.tour.menu_bar" = "PHP Monitor lives in your menu bar. From this menu, you can access most of PHP Monitor's key functionality, including switching the globally linked PHP version, locating config files, and much more."; -"onboarding.tour.faq_hint" = "I recommend that you check out the [README](https://github.com/nicoverbruggen/phpmon/blob/main/README.md) on GitHub: it contains a comprehensive FAQ with various tips and common questions and answers."; +"onboarding.tour.menu_bar" = "PHP Monitor lives in your menu bar. From this menu, you can access most of PHP Monitor's key functionality, including switching the globally linked PHP version, locating config files, installing different PHP versions, and more."; +"onboarding.tour.faq_hint" = "**Questions?** I recommend that you check out the [README](https://github.com/nicoverbruggen/phpmon/blob/main/README.md) on GitHub: it contains a comprehensive FAQ with various tips and common questions and answers."; "onboarding.tour.services.title" = "Manage Homebrew Services"; "onboarding.tour.services" = "Once you click on the menu bar item, you can see at a glance based on the checkmarks or crosses if all of the Homebrew services are up and running. You can also click on a service to quickly toggle it."; "onboarding.tour.domains.title" = "Manage Domains"; "onboarding.tour.domains" = "By opening the Domains window via the menu bar item, you can view which domains are linked and parked, as well as active nginx proxies."; "onboarding.tour.isolation.title" = "Isolate Domains"; -"onboarding.tour.isolation" = "If you have Valet 3 installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain, and that domain only."; +"onboarding.tour.isolation" = "If you have Valet 3 or newer installed, you can even use domain isolation by right-clicking on a given domain in the Domains window. This allows you to pick a specific version of PHP to use for that domain, and that domain only."; +"onboarding.tour.feature_unavailable" = "This feature is currently unavailable and requires Laravel Valet to be installed."; "onboarding.tour.once" = "You will only see the Welcome Tour once. You can re-open the Welcome Tour later via the menu bar icon (available in the menu, under First Aid & Services)."; "onboarding.tour.close" = "Close Tour"; diff --git a/tests/Shared/TestableConfigurations.swift b/tests/Shared/TestableConfigurations.swift index e2b8ce0..ee3fdcb 100644 --- a/tests/Shared/TestableConfigurations.swift +++ b/tests/Shared/TestableConfigurations.swift @@ -24,16 +24,6 @@ class TestableConfigurations { : .fake(.binary), "/opt/homebrew/bin/valet" : .fake(.binary), - "/opt/homebrew/opt/php" - : .fake(.symlink, "/opt/homebrew/Cellar/php/8.2.0"), - "/opt/homebrew/opt/php@8.2/bin/php" - : .fake(.symlink, "/opt/homebrew/Cellar/php/8.2.0/bin/php"), - "/opt/homebrew/Cellar/php/8.2.0/bin/php" - : .fake(.binary), - "/opt/homebrew/Cellar/php/8.2.0/bin/php-config" - : .fake(.binary), - "/opt/homebrew/etc/php/8.2/php-fpm.d/www.conf" - : .fake(.text), "~/.config/valet/config.json" : .fake(.text, """ { @@ -45,31 +35,28 @@ class TestableConfigurations { "loopback": "127.0.0.1" } """), - "/opt/homebrew/etc/php/8.2/php-fpm.d/valet-fpm.conf" - : .fake(.text), - "/opt/homebrew/etc/php/8.2/php.ini" - : .fake(.text), - "/opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini" - : .fake(.text) ], shellOutput: [ + "/opt/homebrew/bin/brew --version" + : .instant(""" + Homebrew 4.0.17-93-gb0dc84b + Homebrew/homebrew-core (git revision 4113c35d80d; last commit 2023-04-06) + Homebrew/homebrew-cask (git revision bcd8ecb74c; last commit 2023-04-06) + """), + "/opt/homebrew/bin/php -v" + : .instant(""" + PHP 8.2.6 (cli) (built: May 11 2023 12:51:38) (NTS) + Copyright (c) The PHP Group + Zend Engine v4.2.6, Copyright (c) Zend Technologies + with Zend OPcache v8.2.6, Copyright (c), by Zend Technologies + with Xdebug v3.2.0, Copyright (c) 2002-2022, by Derick Rethans + """), "sysctl -n sysctl.proc_translated" : .instant("0"), "id -un" : .instant("user"), "which node" : .instant("/opt/homebrew/bin/node"), - "php -v" - : .instant(""" - PHP 8.2.0 (cli) (built: Dec XX 20XX XX:XX:XX) (NTS) - Copyright (c) The PHP Group - Zend Engine vX.X, Copyright (c) Zend Technologies - with Zend OPcache vX.X, Copyright (c), by Zend Technologies - """), - "ls /opt/homebrew/opt | grep php" - : .instant("php"), - "ls /opt/homebrew/opt | grep php@" - : .instant("php@8.2"), "sudo /opt/homebrew/bin/brew services info dnsmasq --json" : .delayed(0.2, """ [ @@ -143,10 +130,10 @@ class TestableConfigurations { cask 'phpmon-dev' do depends_on formula: 'gnu-sed' - version '\(App.shortVersion)_\(App.bundleVersion)' + version '6.0.0_1000' sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' - url 'https://github.com/nicoverbruggen/phpmon/releases/download/v\(App.shortVersion)/phpmon-dev.zip' + url 'https://github.com/nicoverbruggen/phpmon/releases/download/v6.0/phpmon-dev.zip' appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom' name 'PHP Monitor DEV' homepage 'https://phpmon.app' @@ -176,18 +163,31 @@ class TestableConfigurations { : .instant("OK"), ], commandOutput: [ - "/opt/homebrew/bin/php-config --version": "8.2.0", "/opt/homebrew/bin/php -r echo ini_get('memory_limit');": "512M", "/opt/homebrew/bin/php -r echo ini_get('upload_max_filesize');": "512M", "/opt/homebrew/bin/php -r echo ini_get('post_max_size');": "512M", - "/opt/homebrew/bin/php -r echo php_ini_scanned_files();" - : """ - /opt/homebrew/etc/php/8.2/conf.d/php-memory-limits.ini, - """ + "/opt/homebrew/opt/php@8.2/bin/php -v": "OK (no full output needed for testing)", + "/opt/homebrew/opt/php@8.1/bin/php -v": "OK (no full output needed for testing)", + "/opt/homebrew/opt/php@8.0/bin/php -v": "OK (no full output needed for testing)" ], - preferenceOverrides: [:] + preferenceOverrides: [ + .automaticBackgroundUpdateCheck: false + ], + phpVersions: [ + VersionNumber(major: 8, minor: 2, patch: 0), + VersionNumber(major: 8, minor: 1, patch: 0), + VersionNumber(major: 8, minor: 0, patch: 0) + ] ) } + + /** A functional, working system setup (but without Valet). */ + static var workingWithoutValet: TestableConfiguration { + var configuration = TestableConfigurations.working + configuration.filesystem["/opt/homebrew/bin/valet"] = nil + configuration.filesystem["~/.config/valet/config.json"] = nil + return configuration + } } class ShellStrings { diff --git a/tests/feature/InternalSwitcherTest.swift b/tests/feature/InternalSwitcherTest.swift index 9b89eff..ba7fb19 100644 --- a/tests/feature/InternalSwitcherTest.swift +++ b/tests/feature/InternalSwitcherTest.swift @@ -10,21 +10,13 @@ import XCTest final class InternalSwitcherTest: FeatureTestCase { - public func testDefaultPhpFpmPoolRequiresDisabling() async { - ActiveFileSystem.useTestable([ - "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text) - ]) - - assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") - XCTAssertTrue(InternalSwitcher().requiresDisablingOfDefaultPhpFpmPool("8.1")) - } - public func testDefaultPhpFpmPoolIsMoved() async { ActiveFileSystem.useTestable([ "/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf": .fake(.text) ]) - await InternalSwitcher().disableDefaultPhpFpmPool("8.1") + let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") + XCTAssertTrue(outcome) assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") @@ -41,7 +33,8 @@ final class InternalSwitcherTest: FeatureTestCase { contents: "phpmon generated" ) - await InternalSwitcher().disableDefaultPhpFpmPool("8.1") + let outcome = await InternalSwitcher().disableDefaultPhpFpmPool("8.1") + XCTAssertTrue(outcome) assertFileSystemHas("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf.disabled-by-phpmon") assertFileSystemDoesNotHave("/opt/homebrew/etc/php/8.1/php-fpm.d/www.conf") diff --git a/tests/ui/MainMenuTest.swift b/tests/ui/MainMenuTest.swift new file mode 100644 index 0000000..7589554 --- /dev/null +++ b/tests/ui/MainMenuTest.swift @@ -0,0 +1,79 @@ +// +// MainMenuTest.swift +// UI Tests +// +// Created by Nico Verbruggen on 03/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import XCTest + +final class MainMenuTest: UITestCase { + + override func setUpWithError() throws { + continueAfterFailure = false + } + + final func test_can_open_status_menu_item() throws { + let app = launch(openMenu: true) + + assertAllExist([ + // "Switch to PHP 8.2 (php)" should be visible since it is aliased to `php` + app.menuItems["\("mi_php_switch".localized) 8.2 (php)"], + // "Switch to PHP 8.1" should be the non-disabled option + app.menuItems["\("mi_php_switch".localized) 8.1 (php@8.1)"], + // "Switch to PHP 8.0" should be the non-disabled option + app.menuItems["\("mi_php_switch".localized) 8.0 (php@8.0)"], + // We should see the about and quit items + app.menuItems["mi_about".localized], + app.menuItems["mi_quit".localized] + ]) + + sleep(2) + } + + final func test_can_open_domains_list() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_domain_list".localized).click() + } + + final func test_can_open_php_doctor() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_other".localized).click() + app.mainMenuItem(withText: "mi_fa_php_doctor".localized).click() + } + + final func test_can_view_onboarding_flow() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_other".localized).click() + app.mainMenuItem(withText: "mi_view_onboarding".localized).click() + } + + final func test_can_open_about() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_about".localized).click() + } + + final func test_can_open_settings() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_preferences".localized).click() + + assertExists(app.buttons["General"]) + click(app.buttons["General"]) + + assertExists(app.buttons["Appearance"]) + click(app.buttons["Appearance"]) + + assertExists(app.buttons["Visibility"]) + click(app.buttons["Visibility"]) + + assertExists(app.buttons["Notifications"]) + click(app.buttons["Notifications"]) + } + + final func test_can_quit_app() throws { + let app = launch(openMenu: true) + app.mainMenuItem(withText: "mi_quit".localized).click() + } + +} diff --git a/tests/ui/StartupTest.swift b/tests/ui/StartupTest.swift index 369cf26..2c00802 100644 --- a/tests/ui/StartupTest.swift +++ b/tests/ui/StartupTest.swift @@ -20,9 +20,7 @@ final class StartupTest: UITestCase { var configuration = TestableConfigurations.working configuration.filesystem["/opt/homebrew/bin/php"] = nil // PHP binary must be missing - let app = XCPMApplication() - app.withConfiguration(configuration) - app.launch() + let app = launch(with: configuration) // Dialog 1: "PHP is not correctly installed" assertAllExist([ @@ -52,9 +50,7 @@ final class StartupTest: UITestCase { var configuration = TestableConfigurations.working configuration.filesystem["/opt/homebrew/etc/php/8.2/php-fpm.d/valet-fpm.conf"] = nil - let app = XCPMApplication() - app.withConfiguration(configuration) - app.launch() + let app = launch(with: configuration) assertExists(app.staticTexts["alert.php_fpm_broken.title".localized], 3.0) click(app.buttons["generic.ok".localized]) @@ -64,30 +60,9 @@ final class StartupTest: UITestCase { var configuration = TestableConfigurations.working configuration.shellOutput["valet --version"] = .instant("Laravel Valet 5.0") - let app = XCPMApplication() - app.withConfiguration(configuration) - app.launch() + let app = launch(with: configuration) assertExists(app.staticTexts["startup.errors.valet_version_not_supported.title".localized], 3.0) click(app.buttons["generic.ok".localized]) } - - final func test_can_open_status_menu_item() throws { - let app = XCPMApplication() - app.withConfiguration(TestableConfigurations.working) - app.launch() - - // Note: If this fails here, make sure the menu bar item can be displayed - // If you use Bartender or something like this, this item may be hidden and tests will fail - app.statusItems.firstMatch.click() - - assertAllExist([ - // "Switch to PHP 8.1 (php)" should be visible since it is aliased to `php` - app.menuItems["\("mi_php_switch".localized) 8.2 (php)"], - // We should see the about and quit items - app.menuItems["mi_about".localized], - app.menuItems["mi_quit".localized] - ]) - sleep(2) - } } diff --git a/tests/ui/UITestCase.swift b/tests/ui/UITestCase.swift index 858b824..4a02651 100644 --- a/tests/ui/UITestCase.swift +++ b/tests/ui/UITestCase.swift @@ -9,7 +9,6 @@ import XCTest class UITestCase: XCTestCase { - /** Launches the app and opens the menu. */ public func launch( openMenu: Bool = false, @@ -50,7 +49,15 @@ class UITestCase: XCTestCase { public func click(_ element: XCUIElement) { element.click() } +} +extension XCPMApplication { + /** + Opens a given menu item found in the menu bar's status item. + */ + public func mainMenuItem(withText text: String) -> XCUIElement { + self.statusItems.firstMatch.menuItems[text].firstMatch + } } extension XCUIElement { diff --git a/tests/unit/Parsers/CaskFileParserTest.swift b/tests/unit/Parsers/CaskFileParserTest.swift index 1b861f6..df065d3 100644 --- a/tests/unit/Parsers/CaskFileParserTest.swift +++ b/tests/unit/Parsers/CaskFileParserTest.swift @@ -49,5 +49,4 @@ class CaskFileParserTest: XCTestCase { XCTAssertTrue(caskFile.properties.keys.contains("url")) XCTAssertTrue(caskFile.properties.keys.contains("appcast")) } - } diff --git a/tests/unit/Parsers/HomebrewUpgradableTest.swift b/tests/unit/Parsers/HomebrewUpgradableTest.swift new file mode 100644 index 0000000..a4adf27 --- /dev/null +++ b/tests/unit/Parsers/HomebrewUpgradableTest.swift @@ -0,0 +1,40 @@ +// +// HomebrewTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 17/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import XCTest + +class HomebrewUpgradableTest: XCTestCase { + static var outdatedFileUrl: URL { + return Bundle(for: Self.self) + .url(forResource: "brew-outdated", withExtension: "json")! + } + + func test_upgradable_php_versions_can_be_parsed() async throws { + ActiveShell.useTestable([ + "/opt/homebrew/bin/brew update >/dev/null && /opt/homebrew/bin/brew outdated --json --formulae" + : .instant(try! String(contentsOf: Self.outdatedFileUrl)) + ]) + + let env = PhpEnvironments.shared + env.cachedPhpInstallations = [ + "8.1": PhpInstallation("8.1.16"), + "8.2": PhpInstallation("8.2.3"), + "7.4": PhpInstallation("7.4.11") + ] + + let data = await BrewFormulaeHandler().loadPhpVersions(loadOutdated: true) + + XCTAssertTrue(data.contains(where: { formula in + formula.installedVersion == "8.1.16" && formula.upgradeVersion == "8.1.17" + })) + + XCTAssertTrue(data.contains(where: { formula in + formula.installedVersion == "8.2.3" && formula.upgradeVersion == "8.2.4" + })) + } +} diff --git a/tests/unit/Test Files/brew/brew-outdated.json b/tests/unit/Test Files/brew/brew-outdated.json new file mode 100644 index 0000000..79126ca --- /dev/null +++ b/tests/unit/Test Files/brew/brew-outdated.json @@ -0,0 +1,169 @@ +{ + "formulae": [ + { + "name": "cmake", + "installed_versions": [ + "3.25.2" + ], + "current_version": "3.26.0", + "pinned": false, + "pinned_version": null + }, + { + "name": "cmocka", + "installed_versions": [ + "1.1.5" + ], + "current_version": "1.1.7", + "pinned": false, + "pinned_version": null + }, + { + "name": "glib", + "installed_versions": [ + "2.74.6" + ], + "current_version": "2.76.0", + "pinned": false, + "pinned_version": null + }, + { + "name": "harfbuzz", + "installed_versions": [ + "7.0.1" + ], + "current_version": "7.1.0", + "pinned": false, + "pinned_version": null + }, + { + "name": "httpd", + "installed_versions": [ + "2.4.55" + ], + "current_version": "2.4.56", + "pinned": false, + "pinned_version": null + }, + { + "name": "imagemagick", + "installed_versions": [ + "7.1.1-2" + ], + "current_version": "7.1.1-3", + "pinned": false, + "pinned_version": null + }, + { + "name": "libarchive", + "installed_versions": [ + "3.6.2" + ], + "current_version": "3.6.2_1", + "pinned": false, + "pinned_version": null + }, + { + "name": "libsndfile", + "installed_versions": [ + "1.2.0" + ], + "current_version": "1.2.0_1", + "pinned": false, + "pinned_version": null + }, + { + "name": "libvidstab", + "installed_versions": [ + "1.1.0" + ], + "current_version": "1.1.1", + "pinned": false, + "pinned_version": null + }, + { + "name": "libvpx", + "installed_versions": [ + "1.12.0" + ], + "current_version": "1.13.0", + "pinned": false, + "pinned_version": null + }, + { + "name": "node", + "installed_versions": [ + "19.6.0" + ], + "current_version": "19.8.1", + "pinned": false, + "pinned_version": null + }, + { + "name": "pango", + "installed_versions": [ + "1.50.13" + ], + "current_version": "1.50.14", + "pinned": false, + "pinned_version": null + }, + { + "name": "php", + "installed_versions": [ + "8.2.3" + ], + "current_version": "8.2.4", + "pinned": false, + "pinned_version": null + }, + { + "name": "php@8.1", + "installed_versions": [ + "8.1.16" + ], + "current_version": "8.1.17", + "pinned": false, + "pinned_version": null + }, + { + "name": "rclone", + "installed_versions": [ + "1.61.1" + ], + "current_version": "1.62.2", + "pinned": false, + "pinned_version": null + }, + { + "name": "sdl2", + "installed_versions": [ + "2.26.3" + ], + "current_version": "2.26.4", + "pinned": false, + "pinned_version": null + }, + { + "name": "snappy", + "installed_versions": [ + "1.1.9" + ], + "current_version": "1.1.10", + "pinned": false, + "pinned_version": null + }, + { + "name": "tcl-tk", + "installed_versions": [ + "8.6.13" + ], + "current_version": "8.6.13_1", + "pinned": false, + "pinned_version": null + } + ], + "casks": [ + + ] +} diff --git a/tests/unit/Test Files/brew/phpmon-dev.rb b/tests/unit/Test Files/brew/phpmon-dev.rb index 177d11d..60badb9 100644 --- a/tests/unit/Test Files/brew/phpmon-dev.rb +++ b/tests/unit/Test Files/brew/phpmon-dev.rb @@ -1,14 +1,14 @@ cask 'phpmon-dev' do - depends_on formula: 'gnu-sed' - - version '5.7.2_1035' - sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' - - url 'https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip' - appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom' - name 'PHP Monitor DEV' - homepage 'https://phpmon.app' - - app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" - end - \ No newline at end of file + depends_on formula: 'gnu-sed' + + version '5.7.2_1035' + sha256 '1cb147bd1b1fbd52971d90dff577465b644aee7c878f15ede57f46e8f217067a' + + url 'https://github.com/nicoverbruggen/phpmon/releases/download/v5.7.2/phpmon-dev.zip' + appcast 'https://github.com/nicoverbruggen/phpmon/releases.atom' + name 'PHP Monitor DEV' + homepage 'https://phpmon.app' + + app 'PHP Monitor DEV.app', target: "PHP Monitor DEV.app" +end + diff --git a/tests/unit/Testables/Shell/RealShellTest.swift b/tests/unit/Testables/Shell/RealShellTest.swift index 685b0b6..47ac6e1 100644 --- a/tests/unit/Testables/Shell/RealShellTest.swift +++ b/tests/unit/Testables/Shell/RealShellTest.swift @@ -60,7 +60,7 @@ class RealShellTest: XCTestCase { expectation.fulfill() } - wait(for: [expectation], timeout: 5.0) + await fulfillment(of: [expectation], timeout: 5.0) } func test_system_processes_run_in_parallel() async { @@ -74,6 +74,6 @@ class RealShellTest: XCTestCase { } await thing() - wait(for: [expectation], timeout: 1.0) + await fulfillment(of: [expectation], timeout: 5.0) } } diff --git a/tests/unit/Testables/TestableConfigurationTest.swift b/tests/unit/Testables/TestableConfigurationTest.swift index 0b0ef00..511cde4 100644 --- a/tests/unit/Testables/TestableConfigurationTest.swift +++ b/tests/unit/Testables/TestableConfigurationTest.swift @@ -10,6 +10,7 @@ import XCTest class TestableConfigurationTest: XCTestCase { func test_configuration_can_be_saved_as_json() async { + // WORKING var configuration = TestableConfigurations.working try! configuration.toJson().write( @@ -18,6 +19,16 @@ class TestableConfigurationTest: XCTestCase { encoding: .utf8 ) + // WORKING (WITHOUT VALET) + let valetFreeConfiguration = TestableConfigurations.workingWithoutValet + + try! valetFreeConfiguration.toJson().write( + toFile: NSHomeDirectory() + "/.phpmon_fconf_working_no_valet.json", + atomically: true, + encoding: .utf8 + ) + + // NOT WORKING configuration.filesystem["/opt/homebrew/bin/php"] = nil try! configuration.toJson().write( diff --git a/tests/unit/Versions/AppVersionTest.swift b/tests/unit/Versions/AppVersionTest.swift index 2993ee7..30a837c 100644 --- a/tests/unit/Versions/AppVersionTest.swift +++ b/tests/unit/Versions/AppVersionTest.swift @@ -59,32 +59,6 @@ class AppVersionTest: XCTestCase { XCTAssertEqual("dev", version?.suffix) } - func test_tagged_release_omits_zero_patch() { - let version = AppVersion.from("3.5.0_333")! - - XCTAssertEqual(version.tagged, "3.5") - XCTAssertEqual(version.version, "3.5.0") - } - - func test_tagged_release_doesnt_omit_non_zero_patch() { - let version = AppVersion.from("3.5.1_333")! - - XCTAssertEqual(version.tagged, "3.5.1") - XCTAssertEqual(version.version, "3.5.1") - } - - func test_tag_truncation_does_not_affect_major_versions() { - var version = AppVersion.from("5.0_333")! - - XCTAssertEqual(version.tagged, "5.0") - XCTAssertEqual(version.version, "5.0") - - version = AppVersion.from("5.0.0_333")! - - XCTAssertEqual(version.tagged, "5.0") - XCTAssertEqual(version.version, "5.0.0") - } - func test_can_compare_version_numbers() { // Build is newer XCTAssertTrue(AppVersion.from("5.0_101")! > AppVersion.from("5.0_100")!) @@ -98,5 +72,4 @@ class AppVersionTest: XCTestCase { // Build is older XCTAssertFalse(AppVersion.from("5.0_101")! > AppVersion.from("5.0_102")!) } - } diff --git a/tests/unit/Versions/PhpVersionDetectionTest.swift b/tests/unit/Versions/PhpVersionDetectionTest.swift index b496495..ddc1d63 100644 --- a/tests/unit/Versions/PhpVersionDetectionTest.swift +++ b/tests/unit/Versions/PhpVersionDetectionTest.swift @@ -11,7 +11,7 @@ import XCTest class PhpVersionDetectionTest: XCTestCase { func test_can_detect_valid_php_versions() async throws { - let outcome = await PhpEnv.shared.extractPhpVersions( + let outcome = await PhpEnvironments.shared.extractPhpVersions( from: [ "", // empty lines should be omitted "php@8.0",