Compare commits
219 Commits
Author | SHA1 | Date | |
---|---|---|---|
82626b7174 | |||
326e5c58e2 | |||
2848b4dcd2 | |||
a8cf6daa94 | |||
c4749673c9 | |||
20a0059f73 | |||
402e65f82d | |||
29d17b3880 | |||
7f04dd5fcb | |||
0fe9281e3c | |||
f88035b425 | |||
2b7bb3f352 | |||
c7ee4b8838 | |||
ab993efbde | |||
c6f49de70c | |||
8b79dc44d0 | |||
458b952787 | |||
67ec63212c | |||
104f3a7d8c | |||
d2304323fe | |||
76d078735c | |||
1eeba747cf | |||
64c259d804 | |||
b307251f81 | |||
bf610b6c4e | |||
518fb16f23 | |||
24ef7eacfe | |||
050c154894 | |||
18496de104 | |||
a21418a608 | |||
f9ee63ddf6 | |||
b7de54dfa7 | |||
2d0deed4fd | |||
4b1fc1a5ce | |||
227131bd7e | |||
f48a265bbf | |||
7e5cbadc09 | |||
9f608439fc | |||
4bca47a6d9 | |||
c5e8c4c4a6 | |||
85d7c8f9a3 | |||
4c7361e635 | |||
6413287606 | |||
ff8eb4fa04 | |||
6b72d4da65 | |||
b456fdc65c | |||
0e3c9a5a68 | |||
7cf8d4697f | |||
34e5a97155 | |||
ff2c2c9b69 | |||
4e095a5ae5 | |||
6aff283d08 | |||
9c8da2aa1c | |||
5755d608f8 | |||
29fcc66cba | |||
3a826b7e51 | |||
2d8ad9e9bc | |||
64b3c4e9bb | |||
f3b3dcf449 | |||
0bdbc0a056 | |||
4f11f3d8d3 | |||
e1eb61859e | |||
2939a2ab28 | |||
08dcfb36f4 | |||
f8b605f749 | |||
8f1304308d | |||
7e04f8b881 | |||
9dae03a04e | |||
78c24555f7 | |||
41af058661 | |||
bb56e33ee8 | |||
84902f3a42 | |||
8775e70178 | |||
de4cefd1b9 | |||
ebb04001d0 | |||
bd23f65668 | |||
22295ed55a | |||
2bd5b8f79e | |||
34e9e3f829 | |||
016f36a8fd | |||
862add8512 | |||
6dabcd7668 | |||
7b7a5e5236 | |||
a6aecff557 | |||
81ed154db1 | |||
e0b574b33d | |||
f414c723e4 | |||
f3ef1da2bf | |||
f7c716096c | |||
54630c222b | |||
1fc63e0471 | |||
127d5f4494 | |||
13ee618d5c | |||
ed1d7f8aed | |||
063a729d67 | |||
d1498eb070 | |||
7a02fb8a1a | |||
b925262620 | |||
aaa814ac9c | |||
870868bacf | |||
e149a2500e | |||
aef7d4021c | |||
a25f8c9748 | |||
050c489158 | |||
61af6e2dc0 | |||
130bf38dd9 | |||
a6c3f0ceba | |||
defb6e357a | |||
f274e9e004 | |||
a07881a987 | |||
ae9863b962 | |||
d679c7e75c | |||
47d921547f | |||
bc22129399 | |||
98fcb686bf | |||
715b674929 | |||
089ebe7b4e | |||
608525209b | |||
c7eb1d5ce5 | |||
81eb2fee90 | |||
2eea15aac2 | |||
eb664477f9 | |||
e1adcbcde6 | |||
cb504cc3f0 | |||
7c08a2fdfe | |||
3f14754177 | |||
2f47610c85 | |||
b5758dfca7 | |||
2d4112708e | |||
cc6324b692 | |||
15d75a7f98 | |||
32a44524ef | |||
c7c5311ff9 | |||
26a097ed07 | |||
c93f047909 | |||
f82a2120f7 | |||
857cba9f45 | |||
ec49257bcc | |||
5fc159ae5a | |||
9c6a21008a | |||
67a91e1211 | |||
babb734c25 | |||
5dffbf57d1 | |||
21a1d6576e | |||
6456741880 | |||
b08912ce11 | |||
f6d2f09b8d | |||
7285d24ef3 | |||
ac60c66bb9 | |||
9a7575790a | |||
a36487f6e0 | |||
cd5cbccb04 | |||
e7e8658ea6 | |||
20291bf034 | |||
762527ece9 | |||
2c40f433d3 | |||
5ac4817048 | |||
300880f3e5 | |||
78e682688b | |||
208a430066 | |||
5c92d47ff0 | |||
121a227510 | |||
f8642b21e6 | |||
b182218cad | |||
0bb3e5c173 | |||
e541dced4a | |||
04c6cef277 | |||
6d1ea4e93b | |||
4fd48baf63 | |||
1260022d51 | |||
ca72b79924 | |||
1a17a275d4 | |||
202f9ed9d1 | |||
881d863bb8 | |||
ef37876508 | |||
0c52720e55 | |||
744ec95630 | |||
69ff907f4a | |||
57c90c216f | |||
8df126a7b0 | |||
173206bed9 | |||
8fa270fd54 | |||
92509b5a84 | |||
c609022c9b | |||
bbbbe7555b | |||
1216fe4974 | |||
f9363dd35b | |||
7be6a335df | |||
b39b4dc58b | |||
3ceded3456 | |||
6d89f94c92 | |||
ff2eff2a75 | |||
339eafd34d | |||
6115ef02de | |||
8fcaa34cbb | |||
5fa1836693 | |||
24aecb3148 | |||
25f824defd | |||
7936d14440 | |||
c3261b8873 | |||
5923be099f | |||
f92a3f545a | |||
b8e7397233 | |||
5c62f744ad | |||
70ebb2ef59 | |||
d9f4a19b92 | |||
90c8bcc0df | |||
1ef4f0bb81 | |||
0f62b1f1d0 | |||
89642de12e | |||
c6f2167c92 | |||
e8d705e228 | |||
a46602d4b4 | |||
7c3f416789 | |||
8d42e27ef6 | |||
894365488a | |||
3c6d2d74ff | |||
17efb50872 | |||
44800a03a1 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something going wrong? File a bug report!
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: nicoverbruggen
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Required information**
|
||||
- Did you consult the FAQ in the README? [yes/no]
|
||||
- Did you try "Fix My Valet"? [yes/no]
|
||||
- OS: [e.g. macOS Monterey]
|
||||
- PHP Monitor version [e.g. v5.0.1]
|
||||
|
||||
**Additional log**
|
||||
You can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
|
||||
|
||||
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
62
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
name: 🐞 Bug report
|
||||
description: Something going wrong? File a bug report!
|
||||
title: "[Bug] <title>"
|
||||
labels: [bug]
|
||||
assignees: nicoverbruggen
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Open this menu...
|
||||
2. Click here...
|
||||
3. Scroll to...
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **macOS**: (e.g. Ventura 13.3)
|
||||
- **Valet**: (e.g. 4.0)
|
||||
- **PHP Monitor**: (e.g. 5.8)
|
||||
value: |
|
||||
- macOS:
|
||||
- Valet:
|
||||
- PHP Monitor:
|
||||
render: markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Do you have a log file (or a screenshot) or any additional information?
|
||||
description: |
|
||||
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor.
|
||||
|
||||
You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
|
||||
|
||||
(You can attach images or log files by clicking this area to highlight it and then dragging files in.)
|
||||
validations:
|
||||
required: false
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an enhancement.
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: nicoverbruggen
|
||||
|
||||
---
|
||||
|
||||
_Enhancement requests that are not immediately approved will be moved to a discussion instead._
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
31
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
name: 😎 Feature request
|
||||
description: Do you have a great idea for an enhancement that could improve PHP Monitor?
|
||||
title: "[Feature] <title>"
|
||||
labels: [enhancement]
|
||||
assignees: nicoverbruggen
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered. Please make sure you've checked the discussions tab as well. Enhancement requests that are not immediately approved will be moved to a discussion instead, so you will find some there.
|
||||
options:
|
||||
- label: I have searched the existing issues and discussions
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is this feature request related to a problem?
|
||||
description: "A clear and concise description of what the problem is. For example: 'I am always frustrated when...'"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: What would be a user-friendly way of resolving this particular issue?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information or context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
3
.gitignore
vendored
@ -2,4 +2,5 @@ phpmon.xcodeproj/project.xcworkspace
|
||||
phpmon.xcodeproj/xcuserdata
|
||||
PHP Monitor.xcodeproj/project.xcworkspace
|
||||
PHP Monitor.xcodeproj/xcuserdata
|
||||
.DS_Store
|
||||
phpmon-updater/PHP Monitor Self-Updater.app/
|
||||
.DS_Store
|
||||
|
@ -28,15 +28,22 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
|
||||
|
||||
<img src="./docs/build.png" width="404px" alt="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`
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@ -91,12 +91,16 @@
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--cli"
|
||||
isEnabled = "YES">
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_broken.json"
|
||||
isEnabled = "NO">
|
||||
|
@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug.EA"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
codeCoverageEnabled = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
|
||||
BuildableName = "Unit Tests.xctest"
|
||||
BlueprintName = "Unit Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
|
||||
BuildableName = "UI Tests.xctest"
|
||||
BlueprintName = "UI Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
|
||||
BuildableName = "Feature Tests.xctest"
|
||||
BlueprintName = "Feature Tests"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug.EA"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "--v"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--cli"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "--configuration:~/.phpmon_fconf_broken.json"
|
||||
isEnabled = "NO">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "EXTREME_DOCTOR_MODE"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
|
||||
value = ""
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release.EA"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
|
||||
BuildableName = "PHP Monitor.app"
|
||||
BlueprintName = "PHP Monitor"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug.EA">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release.EA"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
|
||||
BuildableName = "PHP Monitor Self-Updater.app"
|
||||
BlueprintName = "PHP Monitor Self-Updater"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
|
||||
BuildableName = "PHP Monitor Self-Updater.app"
|
||||
BlueprintName = "PHP Monitor Self-Updater"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C406A5EF298AD2CE00B5B85A"
|
||||
BuildableName = "PHP Monitor Self-Updater.app"
|
||||
BlueprintName = "PHP Monitor Self-Updater"
|
||||
ReferencedContainer = "container:PHP Monitor.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1400"
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
66
README.md
@ -5,15 +5,13 @@
|
||||
|
||||
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
|
||||
|
||||
<img src="./docs/screenshot.jpg#gh-light-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||
<img src="./docs/screenshot-dark.jpg#gh-dark-mode-only" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||
<img src="./docs/screenshot.jpg" width="1280px" alt="phpmon screenshot (menu bar app)"/>
|
||||
|
||||
<small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
|
||||
|
||||
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
|
||||
|
||||
<img src="./docs/notification.png#gh-light-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
<img src="./docs/notification-dark.png#gh-dark-mode-only" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
<img src="./docs/notification.png" width="370px" alt="phpmon screenshot (notification)"/>
|
||||
|
||||
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
|
||||
|
||||
@ -24,18 +22,17 @@ You can also add new domains as links, isolate sites, manage various services, a
|
||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||
|
||||
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
||||
* macOS 11 Big Sur or later
|
||||
* macOS 12.4 or later (Monterey and Ventura are supported)
|
||||
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
|
||||
* Homebrew `php` formula is installed
|
||||
* Laravel Valet (works with Valet v2, v3 and v4)
|
||||
|
||||
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
|
||||
_Starting with PHP Monitor 6.0, you do not need to have Laravel Valet installed for PHP Monitor to work. To get access to all features of PHP Monitor however, installing Valet is **recommended**._
|
||||
|
||||
For more information, please see [SECURITY.md](./SECURITY.md) to find out which version of the app is currently supported.
|
||||
|
||||
## 🚀 How to install
|
||||
|
||||
Again, make sure you have **[Laravel Valet](https://laravel.com/docs/master/valet)** installed first:
|
||||
Again, if you want to have access to *all features* of PHP Monitor, I recommend installing **[Laravel Valet](https://laravel.com/docs/master/valet)** first:
|
||||
|
||||
```sh
|
||||
composer global require laravel/valet
|
||||
@ -43,22 +40,30 @@ valet install
|
||||
valet trust
|
||||
```
|
||||
|
||||
Once that's done, you can install PHP Monitor via Homebrew (recommended), or (alternatively) you may download the latest release on GitHub.
|
||||
Currently, PHP Monitor is compatible with Laravel Valet v2, v3 and v4. Each of these versions of Valet support slightly different PHP versions, which is why legacy versions remain supported. Please note that some features are not available in older versions of Valet, like site isolation.
|
||||
|
||||
To install via Homebrew, run:
|
||||
#### Manual installation (recommended, first time only)
|
||||
|
||||
Once that's done, you can [download the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest), unzip it and place it in `/Applications`.
|
||||
|
||||
#### Installation via Homebrew
|
||||
|
||||
*Prior to version 5.8, this was the recommended way of installing PHP Monitor.*
|
||||
|
||||
If you prefer to install the app via Homebrew, you can also run the following:
|
||||
|
||||
```sh
|
||||
brew tap nicoverbruggen/homebrew-cask
|
||||
brew install --cask phpmon
|
||||
```
|
||||
|
||||
To upgrade your existing installation, run:
|
||||
## ⬆️ How to update
|
||||
|
||||
```sh
|
||||
brew upgrade phpmon
|
||||
```
|
||||
The recommended method of updating the app to the latest version is to use **the built-in updater**.
|
||||
|
||||
(You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
|
||||
If you have a very slow internet connection, the updater may report that the download has timed out. In that case, you may wish to manually update by [downloading the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest) and placing the app in `/Applications`.
|
||||
|
||||
(You may also use Homebrew to update PHP Monitor, but this will require you to approve the app every time an update is installed. If you use the built-in updater, this won't be necessary.)
|
||||
|
||||
## ⚡️ Launchers (Alfred, Raycast)
|
||||
|
||||
@ -98,10 +103,9 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
||||
> **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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@ -109,33 +113,23 @@ 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.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
|
||||
|
||||
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!
|
||||
</details>
|
||||
@ -273,6 +267,8 @@ This problem is usually resolved by upgrading Valet and running `valet install`
|
||||
|
||||
composer global update
|
||||
valet install
|
||||
|
||||
If you are seeing a 502 (Bad Gateway) error after about 30 seconds or so, your request is likely timing out. You may need to solve a performance issue with your own code.
|
||||
|
||||
</details>
|
||||
|
||||
|
@ -6,9 +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.7 | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x*) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
||||
(*) Preliminary listing. Valet 4 hasn't been released yet and the versions of PHP Valet can work with might still change.
|
||||
| 6.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
@ -16,7 +14,9 @@ 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.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||
| 5.8 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
||||
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
|
||||
|
BIN
assets/affinity/icon-eap.afdesign
Normal file
BIN
assets/affinity/icon-updater.afdesign
Normal file
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 524 KiB |
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 627 KiB |
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 33 KiB |
BIN
phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 811 B |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 97 KiB |
BIN
phpmon-updater/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 301 KiB |
6
phpmon-updater/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
46
phpmon-updater/LaunchControl.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
162
phpmon-updater/Updater.swift
Normal file
@ -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)"
|
||||
}
|
||||
}
|
34
phpmon-updater/Utility.swift
Normal file
@ -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
|
||||
}
|
14
phpmon-updater/main.swift
Normal file
@ -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)
|
5
phpmon-updater/phpmon-updater.entitlements
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 575 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 41 KiB |
68
phpmon/Assets.xcassets/AppIconEAP.appiconset/Contents.json
Normal file
@ -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
|
||||
}
|
||||
}
|
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_128x128@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 644 B |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_256x256@2x.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 62 KiB |
BIN
phpmon/Assets.xcassets/AppIconEAP.appiconset/icon_512x512@2x.png
Normal file
After Width: | Height: | Size: 162 KiB |
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)!
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -16,16 +16,22 @@ public class Paths {
|
||||
public static let shared = Paths()
|
||||
|
||||
internal var baseDir: Paths.HomebrewDir
|
||||
|
||||
private var userName: String! = nil
|
||||
private var userName: String
|
||||
|
||||
init() {
|
||||
// Assume the default directory is correct
|
||||
baseDir = App.architecture != "x86_64" ? .opt : .usr
|
||||
}
|
||||
|
||||
public func loadUser() async {
|
||||
let output = await Shell.pipe("id -un").out
|
||||
userName = String(output.split(separator: "\n")[0])
|
||||
// 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("The current username is `\(userName)`.")
|
||||
}
|
||||
|
||||
public func detectBinaryPaths() {
|
||||
@ -90,6 +96,11 @@ public class Paths {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
public static var caskroomPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
}
|
||||
|
||||
// MARK: - Flexible Binaries
|
||||
// (these can be in multiple locations, so we scan common places because)
|
||||
// (PHP Monitor will not use the user's own PATH)
|
||||
@ -99,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.")
|
||||
@ -110,6 +123,7 @@ public class Paths {
|
||||
public enum HomebrewDir: String {
|
||||
case opt = "/opt/homebrew"
|
||||
case usr = "/usr/local"
|
||||
case usr_hb = "/usr/local/homebrew"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -41,21 +41,24 @@ class RealFileSystem: FileSystemProtocol {
|
||||
}
|
||||
|
||||
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
|
||||
return try FileManager.default.contentsOfDirectory(atPath: path)
|
||||
return try FileManager.default.contentsOfDirectory(atPath: path.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
func getDestinationOfSymlink(_ path: String) throws -> String {
|
||||
return try FileManager.default.destinationOfSymbolicLink(atPath: path)
|
||||
return try FileManager.default.destinationOfSymbolicLink(atPath: path.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
// MARK: - Move & Delete Files
|
||||
|
||||
func move(from path: String, to newPath: String) throws {
|
||||
try FileManager.default.moveItem(atPath: path, toPath: newPath)
|
||||
try FileManager.default.moveItem(
|
||||
atPath: path.replacingTildeWithHomeDirectory,
|
||||
toPath: newPath.replacingTildeWithHomeDirectory
|
||||
)
|
||||
}
|
||||
|
||||
func remove(_ path: String) throws {
|
||||
try FileManager.default.removeItem(atPath: path)
|
||||
try FileManager.default.removeItem(atPath: path.replacingTildeWithHomeDirectory)
|
||||
}
|
||||
|
||||
// MARK: — FS Attributes
|
||||
|
@ -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)
|
||||
}
|
||||
|
69
phpmon/Common/Helpers/FSNotifier.swift
Normal file
@ -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.")
|
||||
}
|
||||
}
|
@ -10,8 +10,8 @@ import UserNotifications
|
||||
|
||||
class LocalNotification {
|
||||
|
||||
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
|
||||
if !Preferences.isEnabled(preference) {
|
||||
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName?) {
|
||||
if preference != nil && !Preferences.isEnabled(preference!) {
|
||||
return
|
||||
}
|
||||
|
||||
|
25
phpmon/Common/Helpers/LoginItemManager.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// LoginItemManager.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 15/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import ServiceManagement
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
class LoginItemManager {
|
||||
func loginItemIsEnabled() -> Bool {
|
||||
return SMAppService.mainApp.status == .enabled
|
||||
}
|
||||
|
||||
func disableLoginItem() {
|
||||
try? SMAppService.mainApp.unregister()
|
||||
}
|
||||
|
||||
func enableLoginItem() {
|
||||
try? SMAppService.mainApp.register()
|
||||
}
|
||||
}
|
17
phpmon/Common/Helpers/Measurements.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -26,3 +26,42 @@ public func system(_ command: String) -> String {
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
Same as the `system` command, but does not return the output.
|
||||
*/
|
||||
public func system_quiet(_ command: String) {
|
||||
let task = Process()
|
||||
task.launchPath = "/bin/sh"
|
||||
task.arguments = ["-c", command]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
_ = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves the username for the currently signed in user via `/usr/bin/id`.
|
||||
This cannot fail or the application will crash.
|
||||
*/
|
||||
public func identity() -> String {
|
||||
let task = Process()
|
||||
task.launchPath = "/usr/bin/id"
|
||||
task.arguments = ["-un"]
|
||||
|
||||
let pipe = Pipe()
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
guard let output = String(
|
||||
data: pipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: String.Encoding.utf8
|
||||
) else {
|
||||
fatalError("Could not retrieve username via `id -un`!")
|
||||
}
|
||||
|
||||
return output.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 []
|
||||
}
|
||||
|
||||
|
@ -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?
|
||||
}
|
@ -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<String>` of installations that are considered valid.
|
||||
*/
|
||||
public func detectPhpVersions() async -> Set<String> {
|
||||
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
|
||||
|
||||
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
|
||||
|
||||
let supportedByValet = Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? []
|
||||
let supportedByValet: Set<String> = {
|
||||
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) })
|
||||
}
|
@ -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)"
|
||||
|
@ -35,6 +35,7 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
- Parameter strict: Whether the patch version check is strict. See more below.
|
||||
|
||||
The strict mode does not matter if a patch version is provided for all versions in the collection.
|
||||
It also does not matter for certain comparisons (e.g. when dealing with wildcards).
|
||||
|
||||
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
|
||||
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
|
||||
@ -45,6 +46,7 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
|
||||
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
|
||||
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
|
||||
|
||||
When checking against actual PHP versions installed by the user (with patch precision), use
|
||||
strict mode.
|
||||
|
||||
@ -52,11 +54,26 @@ public struct PhpVersionNumberCollection: Equatable {
|
||||
|
||||
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
|
||||
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
|
||||
|
||||
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
|
||||
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
|
||||
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
|
||||
*/
|
||||
public func matching(constraint: String, strict: Bool = false) -> [VersionNumber] {
|
||||
if constraint == "*" {
|
||||
return self.versions
|
||||
}
|
||||
|
||||
if let version = VersionNumber.make(from: constraint, type: .wildCardPatch) {
|
||||
// Wildcard for patch (e.g. "7.4.*") must match major and minor (any patch)
|
||||
return self.versions.filter { $0.hasSameMajorAndMinor(version) }
|
||||
}
|
||||
|
||||
if let version = VersionNumber.make(from: constraint, type: .wildCardMinor) {
|
||||
// Strict constraint (e.g. "7.*") -> must only match major (any patch, minor)
|
||||
return self.versions.filter { $0.isSameMajorVersionAs(version) }
|
||||
}
|
||||
|
||||
if let version = VersionNumber.make(from: constraint, type: .versionOnly) {
|
||||
// Strict constraint (e.g. "7.0") -> returns specific version
|
||||
return self.versions.filter { $0.isSameAs(version, strict) }
|
||||
|
@ -39,6 +39,8 @@ public struct VersionNumber: Equatable, Hashable {
|
||||
|
||||
public enum MatchType: String {
|
||||
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case wildCardPatch = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\*)?\z"#
|
||||
case wildCardMinor = #"^(?<major>\d+).(?<minor>\*)?\z"#
|
||||
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
|
||||
@ -64,21 +66,25 @@ public struct VersionNumber: Equatable, Hashable {
|
||||
range: NSRange(location: 0, length: versionString.count)
|
||||
).first
|
||||
|
||||
if match != nil {
|
||||
let major = Int(
|
||||
versionString[Range(match!.range(withName: "major"), in: versionString)!]
|
||||
)!
|
||||
let minor = Int(
|
||||
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
|
||||
)!
|
||||
var patch: Int?
|
||||
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
|
||||
patch = Int(versionString[minorRange])
|
||||
}
|
||||
return Self(major: major, minor: minor, patch: patch)
|
||||
guard let match else { return nil }
|
||||
|
||||
let major = Int(versionString[Range(match.range(withName: "major"), in: versionString)!])!
|
||||
var minor: Int = 0
|
||||
var patch: Int?
|
||||
|
||||
if let minorRange = Range(match.range(withName: "minor"), in: versionString) {
|
||||
let value = versionString[minorRange] as String
|
||||
// Zero is the fallback if a wildcard was used
|
||||
minor = Int(value) ?? 0
|
||||
}
|
||||
|
||||
return nil
|
||||
if let patchRange = Range(match.range(withName: "patch"), in: versionString) {
|
||||
let value = versionString[patchRange] as String
|
||||
// nil is the fallback if a wildcard was used
|
||||
patch = Int(value) ?? nil
|
||||
}
|
||||
|
||||
return Self(major: major, minor: minor, patch: patch)
|
||||
}
|
||||
|
||||
// MARK: Comparison Logic
|
||||
@ -93,6 +99,10 @@ public struct VersionNumber: Equatable, Hashable {
|
||||
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
|
||||
}
|
||||
|
||||
internal func hasSameMajorAndMinor(_ version: VersionNumber) -> Bool {
|
||||
return self.major == version.major && self.minor == version.minor
|
||||
}
|
||||
|
||||
internal func isNewerThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
|
||||
return (
|
||||
self.major > version.major ||
|
||||
|
@ -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!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
134
phpmon/Common/PHP/Switcher/InternalSwitcher+Valet.swift
Normal file
@ -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
|
||||
}
|
@ -22,14 +22,12 @@ class InternalSwitcher: PhpSwitcher {
|
||||
*/
|
||||
func performSwitch(to version: String) async {
|
||||
Log.info("Switching to \(version), unlinking all versions...")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -43,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!")
|
||||
})
|
||||
@ -70,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).")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -115,14 +115,12 @@ class RealShell: ShellProtocol {
|
||||
)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
var args = task.arguments
|
||||
let last: String = "\"" + (args?.popLast() ?? "") + "\""
|
||||
let concat = [self.launchPath] + task.arguments! + [last]
|
||||
let command = concat.joined(separator: " ")
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
|
||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||
$ \(command)
|
||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||
|
||||
[OUT]:
|
||||
\(stdOut)
|
||||
|
@ -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")
|
||||
|
@ -13,11 +13,95 @@ public struct TestableConfiguration: Codable {
|
||||
var filesystem: [String: FakeFile]
|
||||
var shellOutput: [String: BatchFakeShellOutput]
|
||||
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)
|
||||
@ -25,14 +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)
|
||||
|
||||
|
@ -13,10 +13,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<br>
|
||||
<p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
|
||||
<p><b>Do you enjoy using the app? Is it helping you save time?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
|
||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Get the latest on Mastodon.</b> Give me a <a href="https://phpc.social/@nicoverbruggen">follow on Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||
<br>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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?
|
||||
|
||||
|
@ -38,11 +38,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 +64,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 +93,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,10 +116,9 @@ 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 paths.loadUser()
|
||||
await menu.startup()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,182 +0,0 @@
|
||||
//
|
||||
// Updater.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 09/05/2022.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
class AppUpdateChecker {
|
||||
|
||||
public static var enabled: Bool = {
|
||||
return Preferences.isEnabled(.automaticBackgroundUpdateCheck)
|
||||
}()
|
||||
|
||||
public static var isDev: Bool = {
|
||||
return App.version.contains("-dev")
|
||||
}()
|
||||
|
||||
public static func retrieveVersionFromCask(
|
||||
_ initiatedFromBackground: Bool = true
|
||||
) async -> String {
|
||||
let caskFile = App.version.contains("-dev")
|
||||
? Constants.Urls.DevBuildCaskFile.absoluteString
|
||||
: Constants.Urls.StableBuildCaskFile.absoluteString
|
||||
|
||||
var command = "curl -s"
|
||||
|
||||
if initiatedFromBackground {
|
||||
command = "curl -s --max-time 5"
|
||||
}
|
||||
|
||||
return await Shell.pipe(
|
||||
"\(command) '\(caskFile)' | grep version"
|
||||
).out
|
||||
}
|
||||
|
||||
public static func checkIfNewerVersionIsAvailable(
|
||||
initiatedFromBackground: Bool = true
|
||||
) async {
|
||||
if initiatedFromBackground {
|
||||
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
|
||||
Log.info("Automatic updates are disabled. No check will be performed.")
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("Automatic updates are enabled, a check will be performed.")
|
||||
}
|
||||
|
||||
let versionString = await retrieveVersionFromCask(initiatedFromBackground)
|
||||
|
||||
guard let onlineVersion = AppVersion.from(versionString) else {
|
||||
Log.err("We couldn't check for updates!")
|
||||
|
||||
// Only notify about connection issues if the request to check for updates was explicit
|
||||
if !initiatedFromBackground {
|
||||
notifyAboutConnectionIssue()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let currentVersion = AppVersion.fromCurrentVersion()
|
||||
|
||||
handleVersionComparison(
|
||||
currentVersion,
|
||||
onlineVersion,
|
||||
initiatedFromBackground
|
||||
)
|
||||
}
|
||||
|
||||
private static func handleVersionComparison(
|
||||
_ currentVersion: AppVersion,
|
||||
_ onlineVersion: AppVersion,
|
||||
_ background: Bool
|
||||
) {
|
||||
switch onlineVersion.version.versionCompare(currentVersion.version) {
|
||||
case .orderedAscending:
|
||||
Log.info("You are running a newer version of PHP Monitor "
|
||||
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
|
||||
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||
case .orderedDescending:
|
||||
Log.info("There is a newer version (\(onlineVersion)) available! "
|
||||
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
|
||||
notifyAboutNewerVersion(version: onlineVersion)
|
||||
case .orderedSame:
|
||||
if currentVersion.build != nil
|
||||
&& onlineVersion.build != nil
|
||||
&& buildDiffers(currentVersion, onlineVersion, background) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("The installed version (\(currentVersion.computerReadable)) matches the latest release "
|
||||
+ "(\(onlineVersion.computerReadable)).")
|
||||
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||
}
|
||||
}
|
||||
|
||||
private static func buildDiffers(
|
||||
_ currentVersion: AppVersion,
|
||||
_ onlineVersion: AppVersion,
|
||||
_ background: Bool
|
||||
) -> Bool {
|
||||
if Int(onlineVersion.build!)! > Int(currentVersion.build!)! {
|
||||
Log.info("There is a newer build of PHP Monitor available! "
|
||||
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
|
||||
notifyAboutNewerVersion(version: onlineVersion)
|
||||
return true
|
||||
} else if Int(onlineVersion.build!)! < Int(currentVersion.build!)! {
|
||||
Log.info("You are running a newer build of PHP Monitor "
|
||||
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
|
||||
if !background { notifyVersionDoesNotNeedUpgrade() }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private static func notifyVersionDoesNotNeedUpgrade() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.is_latest_version.title".localized,
|
||||
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||
description: ""
|
||||
)
|
||||
.withPrimary(text: "generic.ok".localized)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private static func notifyAboutNewerVersion(version: AppVersion) {
|
||||
let devSuffix = isDev ? "-dev" : ""
|
||||
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
|
||||
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
|
||||
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
|
||||
description: HomebrewDiagnostics.customCaskInstalled
|
||||
? "updater.installation_source.brew".localized(command)
|
||||
: "updater.installation_source.direct".localized
|
||||
)
|
||||
.withPrimary(
|
||||
text: "updater.alerts.buttons.release_notes".localized,
|
||||
action: { vc in
|
||||
vc.close(with: .OK)
|
||||
|
||||
NSWorkspace.shared.open(
|
||||
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
|
||||
)
|
||||
}
|
||||
)
|
||||
.withTertiary(text: "Dismiss", action: { vc in
|
||||
vc.close(with: .OK)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private static func notifyAboutConnectionIssue() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||
App.version
|
||||
)
|
||||
)
|
||||
.withTertiary(
|
||||
text: "updater.alerts.buttons.releases_on_github".localized,
|
||||
action: { _ in
|
||||
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
|
||||
}
|
||||
)
|
||||
.withPrimary(text: "generic.ok".localized)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
215
phpmon/Domain/App/AppUpdater.swift
Normal file
@ -0,0 +1,215 @@
|
||||
//
|
||||
// AppUpdater.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 04/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class AppUpdater {
|
||||
var caskFile: CaskFile!
|
||||
var latestVersionOnline: AppVersion!
|
||||
var interactive: Bool = false
|
||||
|
||||
public func checkForUpdates(userInitiated: Bool) async {
|
||||
self.interactive = userInitiated
|
||||
|
||||
if !interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
|
||||
Log.info("Skipping automatic update check due to user preference.")
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("The app will search for updates...")
|
||||
|
||||
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.")
|
||||
return presentCouldNotRetrieveUpdateIfInteractive()
|
||||
}
|
||||
|
||||
self.caskFile = caskFile
|
||||
|
||||
let currentVersion = AppVersion.fromCurrentVersion()
|
||||
|
||||
guard let onlineVersion = AppVersion.from(caskFile.version) else {
|
||||
Log.err("The version string from the CaskFile could not be read.")
|
||||
return presentCouldNotRetrieveUpdateIfInteractive()
|
||||
}
|
||||
|
||||
latestVersionOnline = onlineVersion
|
||||
Log.info("The latest version read from '\(caskUrl.lastPathComponent)' is: v\(onlineVersion.computerReadable).")
|
||||
|
||||
if latestVersionOnline > currentVersion {
|
||||
presentNewerVersionAvailableAlert()
|
||||
} else if interactive {
|
||||
presentNoNewerVersionAvailableAlert()
|
||||
}
|
||||
}
|
||||
|
||||
private func presentCouldNotRetrieveUpdateIfInteractive() {
|
||||
if interactive {
|
||||
return presentCouldNotRetrieveUpdate()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Alerts
|
||||
|
||||
public func presentNewerVersionAvailableAlert() {
|
||||
let command = App.identifier.contains(".dev")
|
||||
? "brew upgrade phpmon-dev"
|
||||
: "brew upgrade phpmon"
|
||||
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.newer_version_available.title"
|
||||
.localized(latestVersionOnline.humanReadable),
|
||||
subtitle: "updater.alerts.newer_version_available.subtitle"
|
||||
.localized,
|
||||
description: BrewDiagnostics.customCaskInstalled
|
||||
? "updater.installation_source.brew".localized(command)
|
||||
: "updater.installation_source.direct".localized
|
||||
)
|
||||
.withPrimary(
|
||||
text: "updater.alerts.buttons.install".localized,
|
||||
action: { vc in
|
||||
self.cleanupCaskroom()
|
||||
self.prepareForDownload()
|
||||
vc.close(with: .OK)
|
||||
}
|
||||
)
|
||||
.withSecondary(
|
||||
text: "updater.alerts.buttons.release_notes".localized,
|
||||
action: { _ in
|
||||
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
|
||||
vc.close(with: .OK)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
public func presentNoNewerVersionAvailableAlert() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.is_latest_version.title".localized,
|
||||
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||
description: ""
|
||||
)
|
||||
.withPrimary(text: "generic.ok".localized)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
public func presentCouldNotRetrieveUpdate() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||
App.version
|
||||
)
|
||||
)
|
||||
.withTertiary(
|
||||
text: "updater.alerts.buttons.releases_on_github".localized,
|
||||
action: { _ in
|
||||
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
|
||||
}
|
||||
)
|
||||
.withPrimary(text: "generic.ok".localized)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Preparing for Self-Updater
|
||||
|
||||
private func prepareForDownload() {
|
||||
let updater = Bundle.main.resourceURL!.path + "/PHP Monitor Self-Updater.app"
|
||||
|
||||
system_quiet("mkdir -p ~/.config/phpmon/updater 2> /dev/null")
|
||||
|
||||
let updaterDirectory = "~/.config/phpmon/updater"
|
||||
.replacingOccurrences(of: "~", with: NSHomeDirectory())
|
||||
|
||||
system_quiet("cp -R \"\(updater)\" \"\(updaterDirectory)/PHP Monitor Self-Updater.app\"")
|
||||
|
||||
try! FileSystem.writeAtomicallyToFile(
|
||||
"\(updaterDirectory)/update.json",
|
||||
content: "{ \"url\": \"\(caskFile.url)\", \"sha256\": \"\(caskFile.sha256)\" }"
|
||||
)
|
||||
|
||||
let updaterUrl = NSURL(fileURLWithPath: updater, isDirectory: true) as URL
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
|
||||
NSWorkspace.shared.openApplication(at: updaterUrl, configuration: configuration) { _, _ in
|
||||
Log.info("The updater has been launched successfully!")
|
||||
}
|
||||
}
|
||||
|
||||
private func cleanupCaskroom() {
|
||||
let path = Paths.caskroomPath
|
||||
|
||||
if FileSystem.directoryExists(path) {
|
||||
Log.info("Removing the Caskroom directory for PHP Monitor...")
|
||||
do {
|
||||
try FileSystem.remove(path)
|
||||
Log.info("Removed the Caskroom directory at `\(path)`.")
|
||||
} catch {
|
||||
Log.err("Automatically removing the Caskroom directory at `\(path)` failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Checking if Self-Updater Worked
|
||||
|
||||
public static func checkIfUpdateWasPerformed() {
|
||||
// Cleanup the upgrade.success file
|
||||
if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
|
||||
Task { @MainActor in
|
||||
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...")
|
||||
try? FileSystem.remove("~/.config/phpmon/updater/upgrade.success")
|
||||
}
|
||||
|
||||
// Cleanup the previous updater
|
||||
if FileSystem.anyExists("~/.config/phpmon/updater/PHP Monitor Self-Updater.app") {
|
||||
Log.info("A remnant of the self-updater must still be removed...")
|
||||
try? FileSystem.remove("~/.config/phpmon/updater/PHP Monitor Self-Updater.app")
|
||||
}
|
||||
}
|
||||
}
|
@ -8,14 +8,14 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class AppVersion {
|
||||
class AppVersion: Comparable {
|
||||
var version: String
|
||||
var build: String?
|
||||
var build: Int?
|
||||
var suffix: String?
|
||||
|
||||
init(version: String, build: String?, suffix: String? = nil) {
|
||||
self.version = version
|
||||
self.build = build
|
||||
self.build = build == nil ? nil : Int(build!)
|
||||
self.suffix = suffix
|
||||
}
|
||||
|
||||
@ -75,11 +75,27 @@ class AppVersion {
|
||||
}
|
||||
|
||||
var computerReadable: String {
|
||||
return "\(version)_\(build ?? "0")"
|
||||
return "\(version)_\(build ?? 0)"
|
||||
}
|
||||
|
||||
var humanReadable: String {
|
||||
return "\(version) (\(build ?? "???"))"
|
||||
return "\(version) (\(build ?? 0))"
|
||||
}
|
||||
|
||||
// MARK: - Comparable Protocol
|
||||
|
||||
static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
|
||||
let comparisonResult = lhs.version.versionCompare(rhs.version)
|
||||
|
||||
if comparisonResult == .orderedAscending {
|
||||
return true
|
||||
}
|
||||
|
||||
return lhs.build ?? 0 < rhs.build ?? 0
|
||||
}
|
||||
|
||||
static func == (lhs: AppVersion, rhs: AppVersion) -> Bool {
|
||||
lhs.version.versionCompare(rhs.version) == .orderedSame
|
||||
&& lhs.build == rhs.build
|
||||
}
|
||||
}
|
||||
|
@ -43,3 +43,9 @@ struct EnvironmentCheck {
|
||||
return await !self.command()
|
||||
}
|
||||
}
|
||||
|
||||
struct EnvironmentCheckGroup {
|
||||
let name: String
|
||||
let condition: () -> Bool
|
||||
let checks: [EnvironmentCheck]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
])
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|