mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2026-02-04 11:20:08 +01:00
🚀 Version 26.01
This commit is contained in:
34
@tasks/changelog.md
Normal file
34
@tasks/changelog.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
Instructions for the changelog:
|
||||||
|
|
||||||
|
Generate two lists; one containing "What's New" (additions) and one "What's Changed" (bug fixes, modifications).
|
||||||
|
|
||||||
|
Also briefly describe the release in general, e.g. "PHP Monitor X.X is a minor update containing mostly bugfixes." or "PHP Monitor X.X contains a bunch of new features, including X, Y and Z."
|
||||||
|
|
||||||
|
Make sure the changelog does not contain too many references to internal code structure unless necessary, make it understandable to the end user of the application.
|
||||||
|
|
||||||
|
The changelog should be formatted using Markdown like the example, and should be copied to the clipboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
**PHP Monitor X.X** comes with features X, Y, X (brief blurb).
|
||||||
|
|
||||||
|
## What's New in vX.X
|
||||||
|
|
||||||
|
- List item, descriptive.
|
||||||
|
- List item, descriptive.
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
- List item, descriptive.
|
||||||
|
- List item, descriptive.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [ ] Determine latest tag
|
||||||
|
- [ ] Identify diff between latest tag and HEAD
|
||||||
|
- [ ] Go through commits to generate changelog
|
||||||
29
@tasks/pretag.md
Normal file
29
@tasks/pretag.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
Before a release is tagged, you want to make sure that the latest known stable release is known.
|
||||||
|
|
||||||
|
First, identify what has changed between this tagged version and the current HEAD of the branch you wish to merge into `main` as the stable build.
|
||||||
|
|
||||||
|
Tagged releases follow the `vX.Y.Z` naming system, where X is the year, Y is the month version and Z is the patch (usually unspecified unless a patch was released).
|
||||||
|
|
||||||
|
Look for the latest tag on the `main` branch first.
|
||||||
|
|
||||||
|
Make sure all unit tests and UI tests pass prior to finalizing a build. The developer will need to manually check this and report if the tests pass or fail.
|
||||||
|
|
||||||
|
Once this has been confirmed and test pass, a sanity check needs to be done by checking if all of the changes made in the commits since the last release are:
|
||||||
|
|
||||||
|
- Bugfixes for a given issue, without any potential side effects
|
||||||
|
- New features which should have new associated tests
|
||||||
|
- Quality of life improvements that do not require new tests
|
||||||
|
|
||||||
|
If any changes seem incomplete or there's a chance that some functionality may still break despite tests passing (due to some oversight), then no release should be made and those issues should be listed first.
|
||||||
|
|
||||||
|
(These sanity checks can be done manually or assisted by an LLM.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [ ] Do all tests pass? Ask.
|
||||||
|
- [ ] Determine latest tag
|
||||||
|
- [ ] Identify diff between latest tag and HEAD
|
||||||
|
- [ ] Go through commits and sanity check based on instructions
|
||||||
|
- [ ] Determine if ready for a new release
|
||||||
|
- [ ] If ready, generate a short changelog (instructions in ./@changelog.md)
|
||||||
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2019-2023 Nico Verbruggen
|
Copyright (c) 2019-2026 Nico Verbruggen
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -2601,7 +2601,7 @@
|
|||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1420;
|
LastSwiftUpdateCheck = 1420;
|
||||||
LastUpgradeCheck = 2610;
|
LastUpgradeCheck = 2620;
|
||||||
ORGANIZATIONNAME = "Nico Verbruggen";
|
ORGANIZATIONNAME = "Nico Verbruggen";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
C406A5EF298AD2CE00B5B85A = {
|
C406A5EF298AD2CE00B5B85A = {
|
||||||
@@ -3994,7 +3994,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1810;
|
CURRENT_PROJECT_VERSION = 1845;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG = YES;
|
DEBUG = YES;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -4013,7 +4013,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 25.12;
|
MARKETING_VERSION = 26.01;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_MODULE_NAME = PHP_Monitor;
|
PRODUCT_MODULE_NAME = PHP_Monitor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -4038,7 +4038,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1810;
|
CURRENT_PROJECT_VERSION = 1845;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG = NO;
|
DEBUG = NO;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -4057,7 +4057,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 25.12;
|
MARKETING_VERSION = 26.01;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
|
||||||
PRODUCT_MODULE_NAME = PHP_Monitor;
|
PRODUCT_MODULE_NAME = PHP_Monitor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -4220,7 +4220,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1810;
|
CURRENT_PROJECT_VERSION = 1845;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG = YES;
|
DEBUG = YES;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -4239,7 +4239,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 25.12;
|
MARKETING_VERSION = 26.01;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
|
||||||
PRODUCT_MODULE_NAME = PHP_Monitor;
|
PRODUCT_MODULE_NAME = PHP_Monitor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME) EAP";
|
PRODUCT_NAME = "$(TARGET_NAME) EAP";
|
||||||
@@ -4413,7 +4413,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 1810;
|
CURRENT_PROJECT_VERSION = 1845;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEBUG = NO;
|
DEBUG = NO;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
@@ -4432,7 +4432,7 @@
|
|||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
MACOSX_DEPLOYMENT_TARGET = 13.5;
|
||||||
MARKETING_VERSION = 25.12;
|
MARKETING_VERSION = 26.01;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon.eap;
|
||||||
PRODUCT_MODULE_NAME = PHP_Monitor;
|
PRODUCT_MODULE_NAME = PHP_Monitor;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME) EAP";
|
PRODUCT_NAME = "$(TARGET_NAME) EAP";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2610"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "--configuration:~/.phpmon_fconf_working.json"
|
argument = "--configuration:~/.phpmon_fconf_working.json"
|
||||||
isEnabled = "NO">
|
isEnabled = "YES">
|
||||||
</CommandLineArgument>
|
</CommandLineArgument>
|
||||||
<CommandLineArgument
|
<CommandLineArgument
|
||||||
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
|
argument = "--configuration:~/.phpmon_fconf_working_no_valet.json"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2610"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2610"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2610"
|
LastUpgradeVersion = "2620"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.6 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 26 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.6 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|
||||||
## Legacy versions
|
## Legacy versions
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ These versions of PHP Monitor are no longer supported, but if you’re using an
|
|||||||
|
|
||||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||||
|
| 25 | ✅ Universal binary | ❌ | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+) | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.6 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 7.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 7.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||||
|
|||||||
@@ -64,8 +64,25 @@ public class Paths {
|
|||||||
|
|
||||||
// - MARK: Detected Binaries
|
// - MARK: Detected Binaries
|
||||||
|
|
||||||
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
|
public var composer: String? {
|
||||||
public var composer: String?
|
get { _composer.value }
|
||||||
|
set { _composer.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _composer = Locked<String?>(nil)
|
||||||
|
|
||||||
|
private func detectComposerBinary() {
|
||||||
|
if container.filesystem.fileExists("/usr/local/bin/composer") {
|
||||||
|
composer = "/usr/local/bin/composer"
|
||||||
|
} else if container.filesystem.fileExists("/opt/homebrew/bin/composer") {
|
||||||
|
composer = "/opt/homebrew/bin/composer"
|
||||||
|
} else if container.filesystem.fileExists("/usr/local/homebrew/bin/composer") {
|
||||||
|
composer = "/usr/local/homebrew/bin/composer"
|
||||||
|
} else {
|
||||||
|
composer = nil
|
||||||
|
Log.warn("Composer was not found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// - MARK: Paths
|
// - MARK: Paths
|
||||||
|
|
||||||
@@ -118,23 +135,6 @@ public class Paths {
|
|||||||
return preferredShell
|
return preferredShell
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
private func detectComposerBinary() {
|
|
||||||
if container.filesystem.fileExists("/usr/local/bin/composer") {
|
|
||||||
composer = "/usr/local/bin/composer"
|
|
||||||
} else if container.filesystem.fileExists("/opt/homebrew/bin/composer") {
|
|
||||||
composer = "/opt/homebrew/bin/composer"
|
|
||||||
} else if container.filesystem.fileExists("/usr/local/homebrew/bin/composer") {
|
|
||||||
composer = "/usr/local/homebrew/bin/composer"
|
|
||||||
} else {
|
|
||||||
composer = nil
|
|
||||||
Log.warn("Composer was not found.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Enum
|
// MARK: - Enum
|
||||||
|
|
||||||
public enum HomebrewDir: String {
|
public enum HomebrewDir: String {
|
||||||
|
|||||||
@@ -130,12 +130,14 @@ class PhpEnvironments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Information about the currently linked PHP installation. */
|
/** Information about the currently linked PHP installation. */
|
||||||
|
private let _currentInstall = Locked<ActivePhpInstallation?>(nil)
|
||||||
var currentInstall: ActivePhpInstallation? {
|
var currentInstall: ActivePhpInstallation? {
|
||||||
didSet {
|
get { _currentInstall.value }
|
||||||
|
set {
|
||||||
|
// Update the synchronized value
|
||||||
|
_currentInstall.value = newValue
|
||||||
// Let the PHP extension manager, if it exists, know the version changed
|
// Let the PHP extension manager, if it exists, know the version changed
|
||||||
if let version = currentInstall?.version.short {
|
App.shared.phpExtensionManagerWindowController?.view.didUpdatePhpVersion()
|
||||||
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,9 +148,22 @@ class PhpEnvironments {
|
|||||||
but that might not be the case since not everyone keeps their
|
but that might not be the case since not everyone keeps their
|
||||||
software up-to-date.
|
software up-to-date.
|
||||||
|
|
||||||
As such, we take that information from Homebrew.
|
In order for our check to be correct, we query Homebrew locally.
|
||||||
*/
|
*/
|
||||||
static var brewPhpAlias: String?
|
private static let _brewPhpAlias = Locked<String?>(nil)
|
||||||
|
static var brewPhpAlias: String? {
|
||||||
|
get { _brewPhpAlias.value }
|
||||||
|
set { _brewPhpAlias.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Information we were able to discern from the Homebrew info command.
|
||||||
|
*/
|
||||||
|
private let _homebrewPackage = Locked<HomebrewPackage?>(nil)
|
||||||
|
var homebrewPackage: HomebrewPackage! {
|
||||||
|
get { _homebrewPackage.value }
|
||||||
|
set { _homebrewPackage.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
It's possible for the alias to be newer than the actual installed version of PHP.
|
It's possible for the alias to be newer than the actual installed version of PHP.
|
||||||
@@ -191,11 +206,6 @@ class PhpEnvironments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
Information we were able to discern from the Homebrew info command.
|
|
||||||
*/
|
|
||||||
var homebrewPackage: HomebrewPackage! = nil
|
|
||||||
|
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,13 +23,25 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
let filePath: String
|
let filePath: String
|
||||||
|
|
||||||
/// The extensions found in this .ini file.
|
/// The extensions found in this .ini file.
|
||||||
var extensions: [PhpExtension]
|
private let _extensions: Locked<[PhpExtension]>
|
||||||
|
var extensions: [PhpExtension] {
|
||||||
|
get { _extensions.value }
|
||||||
|
set { _extensions.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
/// The actual, structured content of the configuration file.
|
/// The actual, structured content of the configuration file.
|
||||||
var content: Config
|
private let _content: Locked<Config>
|
||||||
|
var content: Config {
|
||||||
|
get { _content.value }
|
||||||
|
set { _content.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
/// The original lines of the file.
|
/// The original lines of the file.
|
||||||
var lines: [String]
|
private let _lines: Locked<[String]>
|
||||||
|
var lines: [String] {
|
||||||
|
get { _lines.value }
|
||||||
|
set { _lines.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolves a PHP configuration file (.ini) */
|
/** Resolves a PHP configuration file (.ini) */
|
||||||
static func from(
|
static func from(
|
||||||
@@ -50,9 +62,13 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
required init(_ container: Container, path: String, contents: String) {
|
required init(_ container: Container, path: String, contents: String) {
|
||||||
self.container = container
|
self.container = container
|
||||||
self.filePath = path
|
self.filePath = path
|
||||||
self.lines = contents.components(separatedBy: "\n")
|
|
||||||
self.extensions = PhpExtension.from(container, lines, filePath: path)
|
let lines = contents.components(separatedBy: "\n")
|
||||||
self.content = Self.parseConfig(lines: lines)
|
|
||||||
|
// We only need to explicitly set our locks here
|
||||||
|
self._lines = Locked(lines)
|
||||||
|
self._extensions = Locked(PhpExtension.from(container, lines, filePath: path))
|
||||||
|
self._content = Locked(Self.parseConfig(lines: lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: API
|
// MARK: API
|
||||||
@@ -89,34 +105,41 @@ class PhpConfigurationFile: CreatedFromFile {
|
|||||||
throw ReplacementErrors.missingKey
|
throw ReplacementErrors.missingKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a thread-safe copy to work with
|
||||||
|
var localLines = lines
|
||||||
|
|
||||||
// Figure out what comes after the assignment
|
// Figure out what comes after the assignment
|
||||||
var components = self
|
var components = localLines[item.lineIndex]
|
||||||
.lines[item.lineIndex]
|
|
||||||
.components(separatedBy: "=")
|
.components(separatedBy: "=")
|
||||||
|
|
||||||
// Replace the value with the new one
|
// Replace the value with the new one
|
||||||
components[1] = components[1]
|
components[1] = components[1]
|
||||||
.replacing(item.value, with: value)
|
.replacing(item.value, with: value)
|
||||||
|
|
||||||
// Replace the specific line
|
// Replace the specific line in the local copy
|
||||||
self.lines[item.lineIndex] = components.joined(separator: "=")
|
localLines[item.lineIndex] = components.joined(separator: "=")
|
||||||
|
|
||||||
// Ensure the watchers aren't tripped up by config changes
|
// Ensure the watchers aren't tripped up by config changes
|
||||||
try await ConfigWatchManager.withSuspended {
|
try await ConfigWatchManager.withSuspended {
|
||||||
// Finally, join the string and save the file atomically again
|
// Finally, join the string and save the file atomically
|
||||||
try self.lines.joined(separator: "\n")
|
try localLines.joined(separator: "\n")
|
||||||
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
.write(toFile: self.filePath, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the original file
|
self.lines = localLines
|
||||||
|
|
||||||
|
// Reload the original file (which will update all properties atomically)
|
||||||
self.reload()
|
self.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reload() {
|
public func reload() {
|
||||||
self.lines = try! String(contentsOfFile: self.filePath)
|
let newLines = try! String(contentsOfFile: self.filePath)
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
self.extensions = PhpExtension.from(container, lines, filePath: self.filePath)
|
|
||||||
self.content = Self.parseConfig(lines: lines)
|
// Update all properties atomically
|
||||||
|
lines = newLines
|
||||||
|
extensions = PhpExtension.from(container, newLines, filePath: self.filePath)
|
||||||
|
content = Self.parseConfig(lines: newLines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Parsing Logic
|
// MARK: Parsing Logic
|
||||||
|
|||||||
@@ -7,32 +7,51 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
@preconcurrency import Dispatch
|
||||||
|
|
||||||
class RealShell: ShellProtocol {
|
class RealShell: ShellProtocol, @unchecked Sendable {
|
||||||
var container: Container
|
init(binPath: String) {
|
||||||
|
self.binPath = binPath
|
||||||
init(container: Container) {
|
self._PATH = RealShell.getPath()
|
||||||
self.container = container
|
self._exports = ""
|
||||||
self.PATH = RealShell.getPath()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private(set) var binPath: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The launch path of the terminal in question that is used.
|
The launch path of the terminal in question that is used.
|
||||||
On macOS, we use /bin/sh since it's pretty fast.
|
On macOS, we use /bin/sh since it's pretty fast.
|
||||||
*/
|
*/
|
||||||
private(set) var launchPath: String = "/bin/sh"
|
private(set) var launchPath: String = "/bin/sh"
|
||||||
|
|
||||||
|
// MARK: - Thread-safe access; public accessor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
For some commands, we need to know what's in the user's PATH.
|
For some commands, we need to know what's in the user's PATH.
|
||||||
The entire PATH is retrieved here, so we can set the PATH in our own terminal as necessary.
|
The entire PATH is retrieved here, so we can set the PATH in our own terminal as necessary.
|
||||||
*/
|
*/
|
||||||
private(set) var PATH: String
|
internal var PATH: String {
|
||||||
|
get { shellQueue.sync { _PATH } }
|
||||||
|
set { shellQueue.sync { _PATH = newValue } }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Exports are additional environment variables set by the user via the custom configuration.
|
Exports are additional environment variables set by the user via the custom configuration.
|
||||||
These are populated when the configuration file is being loaded.
|
These are populated when the configuration file is being loaded.
|
||||||
*/
|
*/
|
||||||
var exports: String = ""
|
internal var exports: String {
|
||||||
|
get { shellQueue.sync { _exports } }
|
||||||
|
set { shellQueue.sync { _exports = newValue } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Thread-safe access; internal values
|
||||||
|
|
||||||
|
/** Thread-safe access to PATH and exports is ensured via this queue. */
|
||||||
|
private let shellQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_queue")
|
||||||
|
private var _PATH: String
|
||||||
|
private var _exports: String
|
||||||
|
|
||||||
|
// MARK: - Methods
|
||||||
|
|
||||||
/** Retrieves the user's PATH by opening an interactive shell and echoing $PATH. */
|
/** Retrieves the user's PATH by opening an interactive shell and echoing $PATH. */
|
||||||
private static func getPath() -> String {
|
private static func getPath() -> String {
|
||||||
@@ -45,15 +64,10 @@ class RealShell: ShellProtocol {
|
|||||||
let pipe = Pipe()
|
let pipe = Pipe()
|
||||||
task.standardOutput = pipe
|
task.standardOutput = pipe
|
||||||
task.launch()
|
task.launch()
|
||||||
|
task.waitUntilExit()
|
||||||
|
|
||||||
let path = String(
|
let path = getStringOutput(from: pipe)
|
||||||
data: pipe.fileHandleForReading.readDataToEndOfFile(),
|
return path.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
encoding: String.Encoding.utf8
|
|
||||||
) ?? ""
|
|
||||||
|
|
||||||
try? pipe.fileHandleForReading.close()
|
|
||||||
|
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +78,7 @@ class RealShell: ShellProtocol {
|
|||||||
var completeCommand = ""
|
var completeCommand = ""
|
||||||
|
|
||||||
// Basic export (PATH)
|
// Basic export (PATH)
|
||||||
completeCommand += "export PATH=\(container.paths.binPath):$PATH && "
|
completeCommand += "export PATH=\(binPath):$PATH && "
|
||||||
|
|
||||||
// Put additional exports (as defined by the user) in between
|
// Put additional exports (as defined by the user) in between
|
||||||
if !self.exports.isEmpty {
|
if !self.exports.isEmpty {
|
||||||
@@ -219,19 +233,30 @@ class RealShell: ShellProtocol {
|
|||||||
process.standardError = errorPipe
|
process.standardError = errorPipe
|
||||||
|
|
||||||
let output = ShellOutput.empty()
|
let output = ShellOutput.empty()
|
||||||
|
|
||||||
|
// Only access `resumed`, `output` from serialQueue to ensure thread safety
|
||||||
let serialQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_output")
|
let serialQueue = DispatchQueue(label: "com.nicoverbruggen.phpmon.shell_output")
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation({ continuation in
|
return try await withCheckedThrowingContinuation({ continuation in
|
||||||
let timeoutTask = Task {
|
// Guard against resuming the continuation twice (race between timeout and termination)
|
||||||
try? await Task.sleep(nanoseconds: timeout.nanoseconds)
|
var resumed = false
|
||||||
// Only terminate if the process is still running
|
|
||||||
if process.isRunning {
|
// Use GCD; we're already using a serial queue so legacy concurrency approach is okay
|
||||||
|
let timeoutTaskTermination = DispatchWorkItem {
|
||||||
|
guard process.isRunning else { return }
|
||||||
|
|
||||||
process.terminationHandler = nil
|
process.terminationHandler = nil
|
||||||
process.terminate()
|
process.terminate()
|
||||||
|
|
||||||
|
if !resumed {
|
||||||
|
resumed = true
|
||||||
continuation.resume(throwing: ShellError.timedOut)
|
continuation.resume(throwing: ShellError.timedOut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Let's make sure that once our timeout occurs, our process is terminated
|
||||||
|
serialQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTaskTermination)
|
||||||
|
|
||||||
// Set up background reading for stdout
|
// Set up background reading for stdout
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
|
outputPipe.fileHandleForReading.readabilityHandler = { fileHandle in
|
||||||
let data = fileHandle.availableData
|
let data = fileHandle.availableData
|
||||||
@@ -255,7 +280,9 @@ class RealShell: ShellProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process.terminationHandler = { process in
|
process.terminationHandler = { process in
|
||||||
timeoutTask.cancel()
|
serialQueue.async {
|
||||||
|
timeoutTaskTermination.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up readability handlers
|
// Clean up readability handlers
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = nil
|
outputPipe.fileHandleForReading.readabilityHandler = nil
|
||||||
@@ -276,9 +303,12 @@ class RealShell: ShellProtocol {
|
|||||||
didReceiveOutput(string, .stdErr)
|
didReceiveOutput(string, .stdErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !resumed {
|
||||||
|
resumed = true
|
||||||
continuation.resume(returning: (process, output))
|
continuation.resume(returning: (process, output))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
process.launch()
|
process.launch()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,9 +137,11 @@ public struct TestableConfiguration: Codable {
|
|||||||
container.overrideWith(config: self)
|
container.overrideWith(config: self)
|
||||||
|
|
||||||
Log.info("Applying temporary preference overrides...")
|
Log.info("Applying temporary preference overrides...")
|
||||||
|
var cachedPrefs = container.preferences.cachedPreferences
|
||||||
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
|
preferenceOverrides.forEach { (key: PreferenceName, value: Any?) in
|
||||||
container.preferences.cachedPreferences[key] = value
|
cachedPrefs[key] = value
|
||||||
}
|
}
|
||||||
|
container.preferences.cachedPreferences = cachedPrefs
|
||||||
|
|
||||||
if Valet.shared.installed {
|
if Valet.shared.installed {
|
||||||
Log.info("Applying fake scanner...")
|
Log.info("Applying fake scanner...")
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Container {
|
class Container: @unchecked Sendable {
|
||||||
// MARK: - Variables
|
// MARK: - Variables
|
||||||
|
|
||||||
// Primary
|
// Primary
|
||||||
private(set) var shell: ShellProtocol!
|
|
||||||
private(set) var filesystem: FileSystemProtocol!
|
private(set) var filesystem: FileSystemProtocol!
|
||||||
private(set) var command: CommandProtocol!
|
|
||||||
private(set) var paths: Paths!
|
private(set) var paths: Paths!
|
||||||
|
private(set) var shell: ShellProtocol!
|
||||||
|
private(set) var command: CommandProtocol!
|
||||||
private(set) var webApi: WebApiProtocol!
|
private(set) var webApi: WebApiProtocol!
|
||||||
|
|
||||||
// Secondary (uses primary instances above)
|
// Secondary (uses primary instances above)
|
||||||
@@ -65,10 +65,10 @@ class Container {
|
|||||||
|
|
||||||
// These are the most basic building blocks. We need these before
|
// These are the most basic building blocks. We need these before
|
||||||
// any of the other classes can be initialized!
|
// any of the other classes can be initialized!
|
||||||
self.shell = RealShell(container: self)
|
|
||||||
self.filesystem = RealFileSystem(container: self)
|
self.filesystem = RealFileSystem(container: self)
|
||||||
self.command = RealCommand()
|
|
||||||
self.paths = Paths(container: self)
|
self.paths = Paths(container: self)
|
||||||
|
self.shell = RealShell(binPath: paths.binPath)
|
||||||
|
self.command = RealCommand()
|
||||||
self.webApi = RealWebApi(container: self)
|
self.webApi = RealWebApi(container: self)
|
||||||
|
|
||||||
if coreOnly {
|
if coreOnly {
|
||||||
@@ -104,6 +104,9 @@ class Container {
|
|||||||
getResponses: webApiGetResponses,
|
getResponses: webApiGetResponses,
|
||||||
postResponses: webApiPostResponses
|
postResponses: webApiPostResponses
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// We will also re-initialize PhpEnvironments due to altered dependencies
|
||||||
|
self.phpEnvs = PhpEnvironments(container: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ extension Startup {
|
|||||||
// Check if we upgraded from a previous version
|
// Check if we upgraded from a previous version
|
||||||
AppUpdater.checkIfUpdateWasPerformed()
|
AppUpdater.checkIfUpdateWasPerformed()
|
||||||
|
|
||||||
|
// Mark app as having successfully booted passing all checks
|
||||||
|
Startup.hasFinishedBooting = true
|
||||||
|
|
||||||
|
// Enable the main menu item
|
||||||
|
MainMenu.shared.statusItem.button?.isEnabled = true
|
||||||
|
|
||||||
// Post-launch stats and update check, but only if not running tests
|
// Post-launch stats and update check, but only if not running tests
|
||||||
await performPostLaunchActions()
|
await performPostLaunchActions()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import NVAlert
|
|||||||
extension Startup {
|
extension Startup {
|
||||||
@MainActor static var startupTimer: Timer?
|
@MainActor static var startupTimer: Timer?
|
||||||
@MainActor static var launchTime: Date?
|
@MainActor static var launchTime: Date?
|
||||||
|
@MainActor static var hasFinishedBooting: Bool = false
|
||||||
|
|
||||||
/** Returns a human-readable version to indicate how many seconds elapsed since boot. */
|
/** Returns a human-readable version to indicate how many seconds elapsed since boot. */
|
||||||
@MainActor static var humanReadableSinceBootTime: String {
|
@MainActor static var humanReadableSinceBootTime: String {
|
||||||
|
|||||||
@@ -203,29 +203,32 @@ class ValetSite: ValetListable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the contents of the composer.json file and determine the notable dependencies,
|
Checks the contents of the composer.json file and determine the notable dependencies,
|
||||||
as well as the requested PHP version. If no composer.json file is found, nothing happens.
|
as well as the requested PHP version. This info is then used to determine project type.
|
||||||
|
|
||||||
|
If no composer.json file is found or is invalid, some features may be unavailable, like
|
||||||
|
for example project type inference based on dependencies.
|
||||||
*/
|
*/
|
||||||
private func determineComposerInformation() {
|
private func determineComposerInformation() {
|
||||||
let path = "\(absolutePath)/composer.json"
|
let path = "\(absolutePath)/composer.json"
|
||||||
|
|
||||||
do {
|
guard container.filesystem.fileExists(path) else {
|
||||||
if container.filesystem.fileExists(path) {
|
return
|
||||||
let decoded = try JSONDecoder().decode(
|
}
|
||||||
ComposerJson.self,
|
|
||||||
from: String(
|
|
||||||
contentsOf: URL(fileURLWithPath: path),
|
|
||||||
encoding: .utf8
|
|
||||||
).data(using: .utf8)!
|
|
||||||
)
|
|
||||||
|
|
||||||
(self.preferredPhpVersion,
|
guard let fileContents = try? String(contentsOf: URL(fileURLWithPath: path), encoding: .utf8),
|
||||||
self.preferredPhpVersionSource) = decoded.getPhpVersion()
|
let jsonData = fileContents.data(using: .utf8) else {
|
||||||
|
Log.err("Could not read the Composer JSON file at: \(path)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let decoded = try? JSONDecoder().decode(ComposerJson.self, from: jsonData) else {
|
||||||
|
Log.err("Could not parse the Composer JSON file at: \(path)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(self.preferredPhpVersion, self.preferredPhpVersionSource) = decoded.getPhpVersion()
|
||||||
self.notableComposerDependencies = decoded.getNotableDependencies()
|
self.notableComposerDependencies = decoded.getNotableDependencies()
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
Log.err("Something went wrong reading the Composer JSON file.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Checks the contents of the .valetphprc file and determine the version.
|
Checks the contents of the .valetphprc file and determine the version.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
statusItem.isVisible = !isRunningSwiftUIPreview
|
statusItem.isVisible = !isRunningSwiftUIPreview
|
||||||
|
statusItem.button?.isEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
weak var menuDelegate: NSMenuDelegate?
|
weak var menuDelegate: NSMenuDelegate?
|
||||||
@@ -238,11 +239,19 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPhpVersionManager() {
|
@objc func openPhpVersionManager() {
|
||||||
|
if !container.phpEnvs.cachedPhpInstallations.isEmpty {
|
||||||
PhpVersionManagerWindowController.show()
|
PhpVersionManagerWindowController.show()
|
||||||
|
} else {
|
||||||
|
Log.err("Skipping opening version manager due to no available PHP versions.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openPhpExtensionManager() {
|
@objc func openPhpExtensionManager() {
|
||||||
|
if !container.phpEnvs.cachedPhpInstallations.isEmpty {
|
||||||
PhpExtensionManagerWindowController.show()
|
PhpExtensionManagerWindowController.show()
|
||||||
|
} else {
|
||||||
|
Log.err("Skipping opening extension manager due to no available PHP versions.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func openDonate() {
|
@objc func openDonate() {
|
||||||
|
|||||||
@@ -11,20 +11,34 @@ import Foundation
|
|||||||
class Preferences {
|
class Preferences {
|
||||||
var container: Container
|
var container: Container
|
||||||
|
|
||||||
var customPreferences: CustomPrefs
|
// MARK: - Preferences
|
||||||
|
|
||||||
var cachedPreferences: [PreferenceName: Any?]
|
var customPreferences: CustomPrefs {
|
||||||
|
get { _customPreferences.value }
|
||||||
|
set { _customPreferences.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedPreferences: [PreferenceName: Any?] {
|
||||||
|
get { _cachedPreferences.value }
|
||||||
|
set { _cachedPreferences.value = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _customPreferences: Locked<CustomPrefs>
|
||||||
|
private let _cachedPreferences: Locked<[PreferenceName: Any?]>
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
public init(container: Container) {
|
public init(container: Container) {
|
||||||
self.container = container
|
self.container = container
|
||||||
Preferences.handleFirstTimeLaunch()
|
Preferences.handleFirstTimeLaunch()
|
||||||
cachedPreferences = Self.cache()
|
|
||||||
customPreferences = CustomPrefs(
|
_cachedPreferences = Locked(Self.cache())
|
||||||
|
_customPreferences = Locked(CustomPrefs(
|
||||||
scanApps: [],
|
scanApps: [],
|
||||||
presets: [],
|
presets: [],
|
||||||
services: [],
|
services: [],
|
||||||
environmentVariables: [:]
|
environmentVariables: [:]
|
||||||
)
|
))
|
||||||
|
|
||||||
if isRunningSwiftUIPreview {
|
if isRunningSwiftUIPreview {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -35,10 +35,14 @@ class WarningManager: ObservableObject {
|
|||||||
/// These warnings are the ones that are ready to be displayed.
|
/// These warnings are the ones that are ready to be displayed.
|
||||||
@Published public var warnings: [Warning] = []
|
@Published public var warnings: [Warning] = []
|
||||||
|
|
||||||
/// This variable is thread-safe and may be modified at any time.
|
/// Thread-safe storage for warnings being evaluated.
|
||||||
/// When all temporary warnings are set, you may broadcast these changes
|
/// When all temporary warnings are set, you may broadcast these changes
|
||||||
/// and they will be sent to the @Published variable via the main thread.
|
/// and they will be sent to the @Published variable via the main thread.
|
||||||
private var temporaryWarnings: [Warning] = []
|
private var temporaryWarnings: [Warning] {
|
||||||
|
get { _temporaryWarnings.value }
|
||||||
|
set { _temporaryWarnings.value = newValue }
|
||||||
|
}
|
||||||
|
private let _temporaryWarnings = Locked<[Warning]>([])
|
||||||
|
|
||||||
public func hasWarnings() -> Bool {
|
public func hasWarnings() -> Bool {
|
||||||
return !warnings.isEmpty
|
return !warnings.isEmpty
|
||||||
@@ -71,22 +75,27 @@ class WarningManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await evaluate()
|
await evaluate()
|
||||||
|
|
||||||
|
// Only rebuild the menu if the app has finished booting
|
||||||
|
// (otherwise the menu may become interactive before all checks are done)
|
||||||
|
if await Startup.hasFinishedBooting {
|
||||||
await MainMenu.shared.rebuild()
|
await MainMenu.shared.rebuild()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Runs through all evaluations and appends any applicable warning results.
|
Runs through all evaluations and appends any applicable warning results.
|
||||||
Will automatically broadcast these warnings.
|
Will automatically broadcast these warnings.
|
||||||
*/
|
*/
|
||||||
private func evaluate() async {
|
private func evaluate() async {
|
||||||
self.temporaryWarnings = []
|
var warnings: [Warning] = []
|
||||||
|
|
||||||
for check in self.evaluations where await check.applies() {
|
for check in self.evaluations where await check.applies() {
|
||||||
Log.info("[DOCTOR] \(check.name) (!)")
|
Log.info("[DOCTOR] \(check.name) (!)")
|
||||||
self.temporaryWarnings.append(check)
|
warnings.append(check)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.temporaryWarnings = warnings
|
||||||
await self.broadcastWarnings()
|
await self.broadcastWarnings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ struct PhpExtensionManagerView: View {
|
|||||||
init() {
|
init() {
|
||||||
self.searchText = ""
|
self.searchText = ""
|
||||||
self.status = BusyStatus.busy()
|
self.status = BusyStatus.busy()
|
||||||
let version = App.shared.container.phpEnvs.currentInstall!.version.short
|
self.manager = BrewExtensionsObservable(phpVersion: Self.getActivePhpVersion())
|
||||||
self.manager = BrewExtensionsObservable(phpVersion: version)
|
|
||||||
self.status.busy = false
|
self.status.busy = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +236,15 @@ struct PhpExtensionManagerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func getActivePhpVersion() -> String {
|
||||||
|
return App.shared.container.phpEnvs.currentInstall?.version.short
|
||||||
|
?? App.shared.container.phpEnvs.cachedPhpInstallations.keys.first!
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUpdatePhpVersion() {
|
||||||
|
self.manager.phpVersion = Self.getActivePhpVersion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ final class StartupTest: UITestCase {
|
|||||||
var configuration = TestableConfigurations.working
|
var configuration = TestableConfigurations.working
|
||||||
configuration.filesystem["/opt/homebrew/bin/php"] = nil // PHP binary must be missing
|
configuration.filesystem["/opt/homebrew/bin/php"] = nil // PHP binary must be missing
|
||||||
|
|
||||||
let app = launch(with: configuration)
|
let app = launch(
|
||||||
|
waitForInitialization: false, // we expect an error during initialization
|
||||||
|
with: configuration
|
||||||
|
)
|
||||||
|
|
||||||
// Dialog 1: "PHP is not correctly installed"
|
// Dialog 1: "PHP is not correctly installed"
|
||||||
assertAllExist([
|
assertAllExist([
|
||||||
@@ -50,7 +53,10 @@ final class StartupTest: UITestCase {
|
|||||||
var configuration = TestableConfigurations.working
|
var configuration = TestableConfigurations.working
|
||||||
configuration.filesystem["/opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf"] = nil
|
configuration.filesystem["/opt/homebrew/etc/php/8.4/php-fpm.d/valet-fpm.conf"] = nil
|
||||||
|
|
||||||
let app = launch(with: configuration)
|
let app = launch(
|
||||||
|
waitForInitialization: false, // we expect an error during initialization
|
||||||
|
with: configuration
|
||||||
|
)
|
||||||
|
|
||||||
assertExists(app.staticTexts["alert.php_fpm_broken.title".localized], 3.0)
|
assertExists(app.staticTexts["alert.php_fpm_broken.title".localized], 3.0)
|
||||||
click(app.buttons["generic.ok".localized])
|
click(app.buttons["generic.ok".localized])
|
||||||
@@ -60,7 +66,10 @@ final class StartupTest: UITestCase {
|
|||||||
var configuration = TestableConfigurations.working
|
var configuration = TestableConfigurations.working
|
||||||
configuration.shellOutput["valet --version"] = .instant("Laravel Valet 5.0")
|
configuration.shellOutput["valet --version"] = .instant("Laravel Valet 5.0")
|
||||||
|
|
||||||
let app = launch(with: configuration)
|
let app = launch(
|
||||||
|
waitForInitialization: false, // we expect an error during initialization
|
||||||
|
with: configuration
|
||||||
|
)
|
||||||
|
|
||||||
assertExists(app.staticTexts["startup.errors.valet_version_not_supported.title".localized], 3.0)
|
assertExists(app.staticTexts["startup.errors.valet_version_not_supported.title".localized], 3.0)
|
||||||
click(app.buttons["generic.ok".localized])
|
click(app.buttons["generic.ok".localized])
|
||||||
|
|||||||
@@ -9,22 +9,41 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class UITestCase: XCTestCase {
|
class UITestCase: XCTestCase {
|
||||||
/** Launches the app and opens the menu. */
|
/**
|
||||||
|
Launches the app and opens the menu.
|
||||||
|
Defaults to waiting for the app to finish initialization.
|
||||||
|
|
||||||
|
- Parameter waitForInitialization: Waits for the PHP Monitor to pass the environment checks (startup).
|
||||||
|
- Parameter openMenu: Attempts to open the status menu when ready; requires passing environment checks.
|
||||||
|
- Parameter configuration: The TestableConfiguration to include when launching PHP Monitor.
|
||||||
|
*/
|
||||||
public func launch(
|
public func launch(
|
||||||
|
waitForInitialization: Bool = true,
|
||||||
openMenu: Bool = false,
|
openMenu: Bool = false,
|
||||||
with configuration: TestableConfiguration? = nil
|
with configuration: TestableConfiguration? = nil,
|
||||||
) -> XCPMApplication {
|
) -> XCPMApplication {
|
||||||
let app = XCPMApplication()
|
let app = XCPMApplication()
|
||||||
let config = configuration ?? TestableConfigurations.working
|
let config = configuration ?? TestableConfigurations.working
|
||||||
app.withConfiguration(config)
|
app.withConfiguration(config)
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Note: If this fails here, make sure the menu bar item can be displayed
|
if waitForInitialization || openMenu {
|
||||||
// If you use Bartender or something like this, this item may be hidden and tests will fail
|
let statusItem = app.statusItems.firstMatch
|
||||||
if openMenu {
|
let isEnabled = NSPredicate(format: "isEnabled == true")
|
||||||
app.statusItems.firstMatch.click()
|
let expectation = expectation(for: isEnabled, evaluatedWith: statusItem, handler: nil)
|
||||||
|
let result = XCTWaiter().wait(for: [expectation], timeout: 15)
|
||||||
|
|
||||||
|
if result == .timedOut {
|
||||||
|
XCTFail("PHP Monitor did not initialize with an available UI element within 15 seconds.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if openMenu {
|
||||||
|
statusItem.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: If this fails here, make sure the menu bar item can be displayed
|
||||||
|
// If you use Bartender or something like this, this item may be hidden and tests will fail
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user