1
0
mirror of https://github.com/laravel/valet.git synced 2026-02-04 16:10:08 +01:00
Files
laravel-valet/cli/Valet/Brew.php
2022-12-26 09:47:58 -05:00

540 lines
15 KiB
PHP

<?php
namespace Valet;
use DomainException;
use Illuminate\Support\Collection;
use PhpFpm;
class Brew
{
const SUPPORTED_PHP_VERSIONS = [
'php',
'php@8.2',
'php@8.1',
'php@8.0',
'php@7.4',
];
const BREW_DISABLE_AUTO_CLEANUP = 'HOMEBREW_NO_INSTALL_CLEANUP=1';
const LATEST_PHP_VERSION = 'php@8.2';
/**
* Create a new Brew instance.
*
* @param CommandLine $cli
* @param Filesystem $files
*/
public function __construct(public CommandLine $cli, public Filesystem $files)
{
}
/**
* Ensure the formula exists in the current Homebrew configuration.
*
* @param string $formula
* @return bool
*/
public function installed(string $formula): bool
{
$result = $this->cli->runAsUser("brew info $formula --json");
// should be a json response, but if not installed then "Error: No available formula ..."
if (starts_with($result, 'Error: No')) {
return false;
}
$details = json_decode($result);
return ! empty($details[0]->installed);
}
/**
* Determine if a compatible PHP version is Homebrewed.
*
* @return bool
*/
public function hasInstalledPhp(): bool
{
$installed = $this->installedPhpFormulae()->first(function ($formula) {
return $this->supportedPhpVersions()->contains($formula);
});
return ! empty($installed);
}
/**
* Get a list of supported PHP versions.
*
* @return Collection
*/
public function supportedPhpVersions(): Collection
{
return collect(static::SUPPORTED_PHP_VERSIONS);
}
/**
* Get a list of installed PHP formulae.
*
* @return Collection
*/
public function installedPhpFormulae(): Collection
{
return collect(
explode(PHP_EOL, $this->cli->runAsUser('brew list --formula | grep php'))
);
}
/**
* Get the aliased formula version from Homebrew.
*
* @return string
*/
public function determineAliasedVersion($formula): string
{
$details = json_decode($this->cli->runAsUser("brew info $formula --json"));
if (! empty($details[0]->aliases[0])) {
return $details[0]->aliases[0];
}
return 'ERROR - NO BREW ALIAS FOUND';
}
/**
* Determine if a compatible nginx version is Homebrewed.
*
* @return bool
*/
public function hasInstalledNginx(): bool
{
return $this->installed('nginx')
|| $this->installed('nginx-full');
}
/**
* Return name of the nginx service installed via Homebrew.
*
* @return string
*/
public function nginxServiceName(): string
{
return $this->installed('nginx-full') ? 'nginx-full' : 'nginx';
}
/**
* Ensure that the given formula is installed.
*
* @param string $formula
* @param array $options
* @param array $taps
* @return void
*/
public function ensureInstalled(string $formula, array $options = [], array $taps = []): void
{
if (! $this->installed($formula)) {
$this->installOrFail($formula, $options, $taps);
}
}
/**
* Install the given formula and throw an exception on failure.
*
* @param string $formula
* @param array $options
* @param array $taps
* @return void
*/
public function installOrFail(string $formula, array $options = [], array $taps = []): void
{
info("Installing {$formula}...");
if (count($taps) > 0) {
$this->tap($taps);
}
output('<info>['.$formula.'] is not installed, installing it now via Brew...</info> 🍻');
if ($formula !== 'php' && starts_with($formula, 'php') && preg_replace('/[^\d]/', '', $formula) < '73') {
warning('Note: older PHP versions may take 10+ minutes to compile from source. Please wait ...');
}
$this->cli->runAsUser(trim(static::BREW_DISABLE_AUTO_CLEANUP.' brew install '.$formula.' '.implode(' ', $options)), function ($exitCode, $errorOutput) use ($formula) {
output($errorOutput);
throw new DomainException('Brew was unable to install ['.$formula.'].');
});
}
/**
* Tap the given formulas.
*
* @param dynamic[string] $formula
* @return void
*/
public function tap($formulas): void
{
$formulas = is_array($formulas) ? $formulas : func_get_args();
foreach ($formulas as $formula) {
$this->cli->passthru(static::BREW_DISABLE_AUTO_CLEANUP.' sudo -u "'.user().'" brew tap '.$formula);
}
}
/**
* Restart the given Homebrew services.
*
* @param dynamic[string] $services
* @return void
*/
public function restartService($services): void
{
$services = is_array($services) ? $services : func_get_args();
foreach ($services as $service) {
if ($this->installed($service)) {
info("Restarting {$service}...");
// first we ensure that the service is not incorrectly running as non-root
$this->cli->quietly('brew services stop '.$service);
// stop the actual/correct sudo version
$this->cli->quietly('sudo brew services stop '.$service);
// start correctly as root
$this->cli->quietly('sudo brew services start '.$service);
}
}
}
/**
* Stop the given Homebrew services.
*
* @param dynamic[string] $services
* @return
*/
public function stopService($services): void
{
$services = is_array($services) ? $services : func_get_args();
foreach ($services as $service) {
if ($this->installed($service)) {
info("Stopping {$service}...");
// first we ensure that the service is not incorrectly running as non-root
$this->cli->quietly('brew services stop '.$service);
// stop the sudo version
$this->cli->quietly('sudo brew services stop '.$service);
// restore folder permissions: for each brew formula, these directories are owned by root:admin
$directories = [
BREW_PREFIX."/Cellar/$service",
BREW_PREFIX."/opt/$service",
BREW_PREFIX."/var/homebrew/linked/$service",
];
$whoami = get_current_user();
foreach ($directories as $directory) {
$this->cli->quietly("sudo chown -R {$whoami}:admin '$directory'");
}
}
}
}
/**
* Determine if php is currently linked.
*
* @return bool
*/
public function hasLinkedPhp(): bool
{
return $this->files->isLink(BREW_PREFIX.'/bin/php');
}
/**
* Get the linked php parsed.
*
* @return array
*/
public function getParsedLinkedPhp(): array
{
if (! $this->hasLinkedPhp()) {
throw new DomainException('Homebrew PHP appears not to be linked. Please run [valet use php@X.Y]');
}
$resolvedPath = $this->files->readLink(BREW_PREFIX.'/bin/php');
return $this->parsePhpPath($resolvedPath);
}
/**
* Gets the currently linked formula by identifying the symlink in the hombrew bin directory.
* Different to ->linkedPhp() in that this will just get the linked directory name,
* whether that is php, php74 or php@7.4.
*
* @return string
*/
public function getLinkedPhpFormula(): string
{
$matches = $this->getParsedLinkedPhp();
return $matches[1].$matches[2];
}
/**
* Determine which version of PHP is linked in Homebrew.
*
* @return string
*/
public function linkedPhp(): string
{
$matches = $this->getParsedLinkedPhp();
$resolvedPhpVersion = $matches[3] ?: $matches[2];
return $this->supportedPhpVersions()->first(
function ($version) use ($resolvedPhpVersion) {
return $this->arePhpVersionsEqual($resolvedPhpVersion, $version);
}, function () use ($resolvedPhpVersion) {
throw new DomainException("Unable to determine linked PHP when parsing '$resolvedPhpVersion'");
});
}
/**
* Extract PHP executable path from PHP Version.
*
* @param string|null $phpVersion For example, "php@8.1"
* @return string
*/
public function getPhpExecutablePath(?string $phpVersion = null): string
{
if (! $phpVersion) {
return BREW_PREFIX.'/bin/php';
}
$phpVersion = PhpFpm::normalizePhpVersion($phpVersion);
// Check the default `/opt/homebrew/opt/php@8.1/bin/php` location first
if ($this->files->exists(BREW_PREFIX."/opt/{$phpVersion}/bin/php")) {
return BREW_PREFIX."/opt/{$phpVersion}/bin/php";
}
// Check the `/opt/homebrew/opt/php71/bin/php` location for older installations
$phpVersion = str_replace(['@', '.'], '', $phpVersion); // php@8.1 to php81
if ($this->files->exists(BREW_PREFIX."/opt/{$phpVersion}/bin/php")) {
return BREW_PREFIX."/opt/{$phpVersion}/bin/php";
}
// Check if the default PHP is the version we are looking for
if ($this->files->isLink(BREW_PREFIX.'/opt/php')) {
$resolvedPath = $this->files->readLink(BREW_PREFIX.'/opt/php');
$matches = $this->parsePhpPath($resolvedPath);
$resolvedPhpVersion = $matches[3] ?: $matches[2];
if ($this->arePhpVersionsEqual($resolvedPhpVersion, $phpVersion)) {
return BREW_PREFIX.'/opt/php/bin/php';
}
}
return BREW_PREFIX.'/bin/php';
}
/**
* Restart the linked PHP-FPM Homebrew service.
*
* @return void
*/
public function restartLinkedPhp(): void
{
$this->restartService($this->getLinkedPhpFormula());
}
/**
* Create the "sudoers.d" entry for running Brew.
*
* @return void
*/
public function createSudoersEntry(): void
{
$this->files->ensureDirExists('/etc/sudoers.d');
$this->files->put('/etc/sudoers.d/brew', 'Cmnd_Alias BREW = '.BREW_PREFIX.'/bin/brew *
%admin ALL=(root) NOPASSWD:SETENV: BREW'.PHP_EOL);
}
/**
* Remove the "sudoers.d" entry for running Brew.
*
* @return void
*/
public function removeSudoersEntry(): void
{
$this->cli->quietly('rm /etc/sudoers.d/brew');
}
/**
* Link passed formula.
*
* @param string $formula
* @param bool $force
* @return string
*/
public function link(string $formula, bool $force = false): string
{
return $this->cli->runAsUser(
sprintf('brew link %s%s', $formula, $force ? ' --force' : ''),
function ($exitCode, $errorOutput) use ($formula) {
output($errorOutput);
throw new DomainException('Brew was unable to link ['.$formula.'].');
}
);
}
/**
* Unlink passed formula.
*
* @param string $formula
* @return string
*/
public function unlink(string $formula): string
{
return $this->cli->runAsUser(
sprintf('brew unlink %s', $formula),
function ($exitCode, $errorOutput) use ($formula) {
output($errorOutput);
throw new DomainException('Brew was unable to unlink ['.$formula.'].');
}
);
}
/**
* Get all the currently running brew services.
*
* @return Collection
*/
public function getAllRunningServices(): Collection
{
return $this->getRunningServicesAsRoot()
->concat($this->getRunningServicesAsUser())
->unique();
}
/**
* Get the currently running brew services as root.
* i.e. /Library/LaunchDaemons (started at boot).
*
* @return Collection
*/
public function getRunningServicesAsRoot(): Collection
{
return $this->getRunningServices();
}
/**
* Get the currently running brew services.
* i.e. ~/Library/LaunchAgents (started at login).
*
* @return \Illuminate\Support\Collection
*/
public function getRunningServicesAsUser(): Collection
{
return $this->getRunningServices(true);
}
/**
* Get the currently running brew services.
*
* @param bool $asUser
* @return Collection
*/
public function getRunningServices(bool $asUser = false): Collection
{
$command = 'brew services list | grep started | awk \'{ print $1; }\'';
$onError = function ($exitCode, $errorOutput) {
output($errorOutput);
throw new DomainException('Brew was unable to check which services are running.');
};
return collect(array_filter(explode(PHP_EOL, $asUser
? $this->cli->runAsUser($command, $onError)
: $this->cli->run('sudo '.$command, $onError)
)));
}
/**
* Tell Homebrew to forcefully remove all PHP versions that Valet supports.
*
* @return string
*/
public function uninstallAllPhpVersions(): string
{
$this->supportedPhpVersions()->each(function ($formula) {
$this->uninstallFormula($formula);
});
return 'PHP versions removed.';
}
/**
* Uninstall a Homebrew app by formula name.
*
* @param string $formula
* @return void
*/
public function uninstallFormula(string $formula): void
{
$this->cli->runAsUser(static::BREW_DISABLE_AUTO_CLEANUP.' brew uninstall --force '.$formula);
$this->cli->run('rm -rf '.BREW_PREFIX.'/Cellar/'.$formula);
}
/**
* Run Homebrew's cleanup commands.
*
* @return string
*/
public function cleanupBrew(): string
{
return $this->cli->runAsUser(
'brew cleanup && brew services cleanup',
function ($exitCode, $errorOutput) {
output($errorOutput);
}
);
}
/**
* Parse homebrew PHP Path.
*
* @param string $resolvedPath
* @return array
*/
public function parsePhpPath(string $resolvedPath): array
{
/**
* Typical homebrew path resolutions are like:
* "../Cellar/php@7.4/7.4.13/bin/php"
* or older styles:
* "../Cellar/php/7.4.9_2/bin/php
* "../Cellar/php55/bin/php.
*/
preg_match('~\w{3,}/(php)(@?\d\.?\d)?/(\d\.\d)?([_\d\.]*)?/?\w{3,}~', $resolvedPath, $matches);
return $matches;
}
/**
* Check if two PHP versions are equal.
*
* @param string $versionA
* @param string $versionB
* @return bool
*/
public function arePhpVersionsEqual(string $versionA, string $versionB): bool
{
$versionANormalized = preg_replace('/[^\d]/', '', $versionA);
$versionBNormalized = preg_replace('/[^\d]/', '', $versionB);
return $versionANormalized === $versionBNormalized;
}
}