mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-08 09:25:35 +08:00
v3 base
This commit is contained in:
46
src/StaticPHP/Doctor/CheckResult.php
Normal file
46
src/StaticPHP/Doctor/CheckResult.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor;
|
||||
|
||||
class CheckResult
|
||||
{
|
||||
public function __construct(private readonly bool $ok, private readonly ?string $message = null, private string $fix_item = '', private array $fix_params = []) {}
|
||||
|
||||
public static function fail(string $message, string $fix_item = '', array $fix_params = []): CheckResult
|
||||
{
|
||||
return new static(false, $message, $fix_item, $fix_params);
|
||||
}
|
||||
|
||||
public static function ok(?string $message = null): CheckResult
|
||||
{
|
||||
return new static(true, $message);
|
||||
}
|
||||
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getFixItem(): string
|
||||
{
|
||||
return $this->fix_item;
|
||||
}
|
||||
|
||||
public function getFixParams(): array
|
||||
{
|
||||
return $this->fix_params;
|
||||
}
|
||||
|
||||
public function isOK(): bool
|
||||
{
|
||||
return $this->ok;
|
||||
}
|
||||
|
||||
public function setFixItem(string $fix_item = '', array $fix_params = []): void
|
||||
{
|
||||
$this->fix_item = $fix_item;
|
||||
$this->fix_params = $fix_params;
|
||||
}
|
||||
}
|
||||
161
src/StaticPHP/Doctor/Doctor.php
Normal file
161
src/StaticPHP/Doctor/Doctor.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
readonly class Doctor
|
||||
{
|
||||
public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT)
|
||||
{
|
||||
// debug shows all loaded doctor items
|
||||
$items = DoctorLoader::getDoctorItems();
|
||||
$names = array_map(fn ($i) => $i->item_name, array_map(fn ($x) => $x[0], $items));
|
||||
logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all valid check items.
|
||||
* @return bool true if all checks passed, false otherwise
|
||||
*/
|
||||
public function checkAll(bool $interactive = true): bool
|
||||
{
|
||||
if ($interactive) {
|
||||
InteractiveTerm::notice('Starting doctor checks ...');
|
||||
}
|
||||
foreach ($this->getValidCheckList() as $check) {
|
||||
if (!$this->checkItem($check)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a single check item.
|
||||
*
|
||||
* @param CheckItem|string $check The check item to be checked
|
||||
* @return bool True if the check passed or was fixed, false otherwise
|
||||
*/
|
||||
public function checkItem(CheckItem|string $check): bool
|
||||
{
|
||||
if (is_string($check)) {
|
||||
$found = null;
|
||||
foreach (DoctorLoader::getDoctorItems() as $item) {
|
||||
if ($item[0]->item_name === $check) {
|
||||
$found = $item[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($found === null) {
|
||||
$this->output?->writeln("<error>Check item '{$check}' not found.</error>");
|
||||
return false;
|
||||
}
|
||||
$check = $found;
|
||||
}
|
||||
$this->output?->write("Checking <comment>{$check->item_name}</comment> ... ");
|
||||
|
||||
// call check
|
||||
$result = call_user_func($check->callback);
|
||||
|
||||
if ($result === null) {
|
||||
$this->output?->writeln('skipped');
|
||||
return true;
|
||||
}
|
||||
if (!$result instanceof CheckResult) {
|
||||
$this->output?->writeln('<error>Skipped due to invalid return value</error>');
|
||||
return true;
|
||||
}
|
||||
if ($result->isOK()) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->output?->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓'));
|
||||
return true;
|
||||
}
|
||||
$this->output?->writeln('<error>' . $result->getMessage() . '</error>');
|
||||
|
||||
// if the check item is not fixable, fail immediately
|
||||
if ($result->getFixItem() === '') {
|
||||
$this->output?->writeln('This check item can not be fixed automatically !');
|
||||
return false;
|
||||
}
|
||||
// unknown fix item
|
||||
if (!DoctorLoader::getFixItem($result->getFixItem())) {
|
||||
$this->output?->writeln("<error>Internal error: Unknown fix item: {$result->getFixItem()}</error>");
|
||||
return false;
|
||||
}
|
||||
// skip fix
|
||||
if ($this->auto_fix === FIX_POLICY_DIE) {
|
||||
$this->output?->writeln('<comment>Auto-fix is disabled. Please fix this issue manually.</comment>');
|
||||
return false;
|
||||
}
|
||||
// prompt for fix
|
||||
if ($this->auto_fix === FIX_POLICY_PROMPT && !confirm('Do you want to try to fix this issue now?')) {
|
||||
$this->output?->writeln('<comment>You canceled fix.</comment>');
|
||||
return false;
|
||||
}
|
||||
// perform fix
|
||||
InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... ");
|
||||
Shell::passthruCallback(function () {
|
||||
InteractiveTerm::advance();
|
||||
});
|
||||
// $this->output?->writeln("Fixing <comment>{$check->item_name}</comment> ... ");
|
||||
if ($this->emitFix($result->getFixItem(), $result->getFixParams())) {
|
||||
InteractiveTerm::finish('Fix applied successfully!');
|
||||
return true;
|
||||
}
|
||||
InteractiveTerm::finish('Failed to apply fix!', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
private function emitFix(string $fix_item, array $fix_item_params = []): bool
|
||||
{
|
||||
keyboard_interrupt_register(function () {
|
||||
$this->output?->writeln('<error>You cancelled fix</error>');
|
||||
});
|
||||
try {
|
||||
return ApplicationContext::invoke(DoctorLoader::getFixItem($fix_item), $fix_item_params);
|
||||
} catch (SPCException $e) {
|
||||
$this->output?->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->output?->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} finally {
|
||||
keyboard_interrupt_unregister();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of valid check items for current environment.
|
||||
*/
|
||||
private function getValidCheckList(): iterable
|
||||
{
|
||||
foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) {
|
||||
/* @var CheckItem $item */
|
||||
// optional check
|
||||
if ($optional !== null && !call_user_func($optional)) {
|
||||
continue; // skip this when the optional check is false
|
||||
}
|
||||
// limit_os check
|
||||
if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) {
|
||||
continue;
|
||||
}
|
||||
// skipped items by env
|
||||
$skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: ''));
|
||||
if (in_array($item->item_name, $skip_items)) {
|
||||
continue; // skip this item
|
||||
}
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/StaticPHP/Doctor/DoctorLoader.php
Normal file
123
src/StaticPHP/Doctor/DoctorLoader.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class DoctorLoader
|
||||
{
|
||||
/**
|
||||
* @var array<int, array{0: CheckItem, 1: callable}> $doctor_items Loaded doctor check item instances
|
||||
*/
|
||||
private static array $doctor_items = [];
|
||||
|
||||
/**
|
||||
* @var array<string, callable> $fix_items loaded doctor fix item instances
|
||||
*/
|
||||
private static array $fix_items = [];
|
||||
|
||||
/**
|
||||
* Load doctor check items from PSR-4 directory.
|
||||
*
|
||||
* @param string $dir Directory path
|
||||
* @param string $base_namespace Base namespace for dir's PSR-4 mapping
|
||||
* @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload)
|
||||
*/
|
||||
public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void
|
||||
{
|
||||
$classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require);
|
||||
foreach ($classes as $class) {
|
||||
self::loadFromClass($class, false);
|
||||
}
|
||||
|
||||
// sort check items by level
|
||||
usort(self::$doctor_items, function ($a, $b) {
|
||||
return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load doctor check items from a class.
|
||||
*
|
||||
* @param string $class Class name to load doctor check items from
|
||||
* @param bool $sort Whether to re-sort Doctor items (default: true)
|
||||
*/
|
||||
public static function loadFromClass(string $class, bool $sort = true): void
|
||||
{
|
||||
// passthough to all the functions if #[OptionalCheck] is set on class level
|
||||
$optional_passthrough = null;
|
||||
$reflection = new \ReflectionClass($class);
|
||||
$class_instance = $reflection->newInstance();
|
||||
// parse #[OptionalCheck]
|
||||
$optional = $reflection->getAttributes(OptionalCheck::class)[0] ?? null;
|
||||
if ($optional !== null) {
|
||||
/** @var OptionalCheck $instance */
|
||||
$instance = $optional->newInstance();
|
||||
if (is_callable($instance->check)) {
|
||||
$optional_passthrough = $instance->check;
|
||||
}
|
||||
}
|
||||
|
||||
$doctor_items = [];
|
||||
$fix_item_map = [];
|
||||
|
||||
// finx check items and fix items from methods in class
|
||||
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
// passthrough for this method if #[OptionalCheck] is set on method level
|
||||
$optional = $optional_passthrough ?? null;
|
||||
foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) {
|
||||
$optional_check = $method_attr->newInstance();
|
||||
if (is_callable($optional_check->check)) {
|
||||
$optional = $optional_check->check;
|
||||
}
|
||||
}
|
||||
|
||||
// parse #[CheckItem]
|
||||
foreach ($method->getAttributes(CheckItem::class) as $attr) {
|
||||
/** @var CheckItem $instance */
|
||||
$instance = $attr->newInstance();
|
||||
$instance->callback = [$class_instance, $method->getName()];
|
||||
// put CheckItem instance and optional check callback (or null) to $doctor_items
|
||||
$doctor_items[] = [$instance, $optional];
|
||||
}
|
||||
|
||||
// parse #[FixItem]
|
||||
$fix_item = $method->getAttributes(FixItem::class)[0] ?? null;
|
||||
if ($fix_item !== null) {
|
||||
$instance = $fix_item->newInstance();
|
||||
$fix_item_map[$instance->name] = [$class_instance, $method->getName()];
|
||||
}
|
||||
}
|
||||
|
||||
// add to array
|
||||
self::$doctor_items = array_merge(self::$doctor_items, $doctor_items);
|
||||
self::$fix_items = array_merge(self::$fix_items, $fix_item_map);
|
||||
|
||||
if ($sort) {
|
||||
// sort check items by level
|
||||
usort(self::$doctor_items, function ($a, $b) {
|
||||
return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns loaded doctor check items.
|
||||
*
|
||||
* @return array<int, array{0: CheckItem, 1: callable}>
|
||||
*/
|
||||
public static function getDoctorItems(): array
|
||||
{
|
||||
return self::$doctor_items;
|
||||
}
|
||||
|
||||
public static function getFixItem(string $name): ?callable
|
||||
{
|
||||
return self::$fix_items[$name] ?? null;
|
||||
}
|
||||
}
|
||||
73
src/StaticPHP/Doctor/Item/LinuxMuslCheck.php
Normal file
73
src/StaticPHP/Doctor/Item/LinuxMuslCheck.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Toolchain\MuslToolchain;
|
||||
use StaticPHP\Toolchain\ZigToolchain;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
#[OptionalCheck([self::class, 'optionalCheck'])]
|
||||
class LinuxMuslCheck
|
||||
{
|
||||
public static function optionalCheck(): bool
|
||||
{
|
||||
return getenv('SPC_TOOLCHAIN') === MuslToolchain::class ||
|
||||
(getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist());
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
#[CheckItem('if musl-wrapper is installed', limit_os: 'Linux', level: 800)]
|
||||
public function checkMusl(): CheckResult
|
||||
{
|
||||
$musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m'));
|
||||
if (file_exists($musl_wrapper_lib) && (file_exists('/usr/local/musl/lib/libc.a') || getenv('SPC_TOOLCHAIN') === ZigToolchain::class)) {
|
||||
return CheckResult::ok();
|
||||
}
|
||||
return CheckResult::fail('musl-wrapper is not installed on your system', 'fix-musl-wrapper');
|
||||
}
|
||||
|
||||
#[CheckItem('if musl-cross-make is installed', limit_os: 'Linux', level: 799)]
|
||||
public function checkMuslCrossMake(): CheckResult
|
||||
{
|
||||
if (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()) {
|
||||
return CheckResult::ok();
|
||||
}
|
||||
$arch = arch2gnu(php_uname('m'));
|
||||
$cross_compile_lib = "/usr/local/musl/{$arch}-linux-musl/lib/libc.a";
|
||||
$cross_compile_gcc = "/usr/local/musl/bin/{$arch}-linux-musl-gcc";
|
||||
if (file_exists($cross_compile_lib) && file_exists($cross_compile_gcc)) {
|
||||
return CheckResult::ok();
|
||||
}
|
||||
return CheckResult::fail('musl-cross-make is not installed on your system', 'fix-musl-cross-make');
|
||||
}
|
||||
|
||||
#[FixItem('fix-musl-wrapper')]
|
||||
public function fixMusl(): bool
|
||||
{
|
||||
// TODO: implement musl-wrapper installation
|
||||
// This should:
|
||||
// 1. Download musl source using Downloader::downloadSource()
|
||||
// 2. Extract the source using FileSystem::extractSource()
|
||||
// 3. Apply CVE patches using SourcePatcher::patchFile()
|
||||
// 4. Build and install musl wrapper
|
||||
// 5. Add path using putenv instead of editing /etc/profile
|
||||
return false;
|
||||
}
|
||||
|
||||
#[FixItem('fix-musl-cross-make')]
|
||||
public function fixMuslCrossMake(): bool
|
||||
{
|
||||
// TODO: implement musl-cross-make installation
|
||||
// This should:
|
||||
// 1. Install musl-toolchain package using PackageManager::installPackage()
|
||||
// 2. Copy toolchain files to /usr/local/musl
|
||||
return false;
|
||||
}
|
||||
}
|
||||
106
src/StaticPHP/Doctor/Item/MacOSToolCheck.php
Normal file
106
src/StaticPHP/Doctor/Item/MacOSToolCheck.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Util\System\MacOSUtil;
|
||||
|
||||
class MacOSToolCheck
|
||||
{
|
||||
public const array REQUIRED_COMMANDS = [
|
||||
'curl',
|
||||
'make',
|
||||
'bison',
|
||||
're2c',
|
||||
'flex',
|
||||
'pkg-config',
|
||||
'git',
|
||||
'autoconf',
|
||||
'automake',
|
||||
'tar',
|
||||
'libtool',
|
||||
'unzip',
|
||||
'xz',
|
||||
'gzip',
|
||||
'bzip2',
|
||||
'cmake',
|
||||
'glibtoolize',
|
||||
];
|
||||
|
||||
#[CheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)]
|
||||
public function checkBrew(): ?CheckResult
|
||||
{
|
||||
if (($path = MacOSUtil::findCommand('brew')) === null) {
|
||||
return CheckResult::fail('Homebrew is not installed', 'brew');
|
||||
}
|
||||
if ($path !== '/opt/homebrew/bin/brew' && getenv('GNU_ARCH') === 'aarch64') {
|
||||
return CheckResult::fail('Current homebrew (/usr/local/bin/homebrew) is not installed for M1 Mac, please re-install homebrew in /opt/homebrew/ !');
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[CheckItem('if necessary tools are installed', limit_os: 'Darwin')]
|
||||
public function checkCliTools(): ?CheckResult
|
||||
{
|
||||
$missing = [];
|
||||
foreach (self::REQUIRED_COMMANDS as $cmd) {
|
||||
if (MacOSUtil::findCommand($cmd) === null) {
|
||||
$missing[] = $cmd;
|
||||
}
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
return CheckResult::fail('missing system commands: ' . implode(', ', $missing), 'build-tools', [$missing]);
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[CheckItem('if bison version is 3.0 or later', limit_os: 'Darwin')]
|
||||
public function checkBisonVersion(array $command_path = []): ?CheckResult
|
||||
{
|
||||
// if the bison command is /usr/bin/bison, it is the system bison that may be too old
|
||||
if (($bison = MacOSUtil::findCommand('bison', $command_path)) === null) {
|
||||
return CheckResult::fail('bison is not installed or too old', 'build-tools', [['bison']]);
|
||||
}
|
||||
// check version: bison (GNU Bison) x.y(.z)
|
||||
$version = shell()->execWithResult("{$bison} --version", false);
|
||||
if (preg_match('/bison \(GNU Bison\) (\d+)\.(\d+)(?:\.(\d+))?/', $version[1][0], $matches)) {
|
||||
$major = (int) $matches[1];
|
||||
// major should be 3 or later
|
||||
if ($major < 3) {
|
||||
// find homebrew keg-only bison
|
||||
if ($command_path !== []) {
|
||||
return CheckResult::fail("Current {$bison} version is too old: " . $matches[0]);
|
||||
}
|
||||
return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin']);
|
||||
}
|
||||
return CheckResult::ok($matches[0]);
|
||||
}
|
||||
return CheckResult::fail('bison version cannot be determined');
|
||||
}
|
||||
|
||||
#[FixItem('brew')]
|
||||
public function fixBrew(): bool
|
||||
{
|
||||
shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"');
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('build-tools')]
|
||||
public function fixBuildTools(array $missing): bool
|
||||
{
|
||||
$replacement = [
|
||||
'glibtoolize' => 'libtool',
|
||||
];
|
||||
foreach ($missing as $cmd) {
|
||||
if (isset($replacement[$cmd])) {
|
||||
$cmd = $replacement[$cmd];
|
||||
}
|
||||
shell()->exec('brew install --formula ' . escapeshellarg($cmd));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
src/StaticPHP/Doctor/Item/OSCheck.php
Normal file
23
src/StaticPHP/Doctor/Item/OSCheck.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
class OSCheck
|
||||
{
|
||||
#[CheckItem('if current OS are supported', level: 1000)]
|
||||
public function checkOS(): ?CheckResult
|
||||
{
|
||||
if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'Windows'])) {
|
||||
return CheckResult::fail('Current OS is not supported: ' . PHP_OS_FAMILY);
|
||||
}
|
||||
$distro = PHP_OS_FAMILY === 'Linux' ? (' ' . LinuxUtil::getOSRelease()['dist']) : '';
|
||||
$known_distro = PHP_OS_FAMILY !== 'Linux' || in_array(LinuxUtil::getOSRelease()['dist'], LinuxUtil::getSupportedDistros());
|
||||
return CheckResult::ok(PHP_OS_FAMILY . ' ' . php_uname('m') . $distro . ', supported' . ($known_distro ? '' : ' (but not tested on this distro)'));
|
||||
}
|
||||
}
|
||||
53
src/StaticPHP/Doctor/Item/PkgConfigCheck.php
Normal file
53
src/StaticPHP/Doctor/Item/PkgConfigCheck.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\PkgConfigUtil;
|
||||
|
||||
#[OptionalCheck([self::class, 'optionalCheck'])]
|
||||
class PkgConfigCheck
|
||||
{
|
||||
public static function optionalCheck(): bool
|
||||
{
|
||||
return SystemTarget::getTargetOS() !== 'Windows';
|
||||
}
|
||||
|
||||
#[CheckItem('if pkg-config is installed or built', level: 800)]
|
||||
public function check(): CheckResult
|
||||
{
|
||||
if (!($pkgconf = PkgConfigUtil::findPkgConfig())) {
|
||||
return CheckResult::fail('pkg-config is not installed or built', 'install-pkg-config');
|
||||
}
|
||||
return CheckResult::ok($pkgconf);
|
||||
}
|
||||
|
||||
#[CheckItem('if pkg-config is functional', level: 799)]
|
||||
public function checkFunctional(): CheckResult
|
||||
{
|
||||
$pkgconf = PkgConfigUtil::findPkgConfig();
|
||||
[$ret, $output] = shell()->execWithResult("{$pkgconf} --version", false);
|
||||
if ($ret !== 0) {
|
||||
return CheckResult::fail('pkg-config is not functional', 'install-pkg-config');
|
||||
}
|
||||
return CheckResult::ok(trim($output[0]));
|
||||
}
|
||||
|
||||
#[FixItem('install-pkg-config')]
|
||||
public function fix(): bool
|
||||
{
|
||||
ApplicationContext::set('elephant', true);
|
||||
$installer = new PackageInstaller(['dl-prefer-binary' => true]);
|
||||
$installer->addInstallPackage('pkg-config');
|
||||
$installer->run(false, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
35
src/StaticPHP/Doctor/Item/Re2cVersionCheck.php
Normal file
35
src/StaticPHP/Doctor/Item/Re2cVersionCheck.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
|
||||
class Re2cVersionCheck
|
||||
{
|
||||
#[CheckItem('if re2c version >= 1.0.3', limit_os: 'Linux', level: 20)]
|
||||
#[CheckItem('if re2c version >= 1.0.3', limit_os: 'Darwin', level: 20)]
|
||||
public function checkRe2cVersion(): ?CheckResult
|
||||
{
|
||||
$ver = shell(false)->execWithResult('re2c --version', false);
|
||||
// match version: re2c X.X(.X)
|
||||
if ($ver[0] !== 0 || !preg_match('/re2c\s+(\d+\.\d+(\.\d+)?)/', $ver[1][0], $matches)) {
|
||||
return CheckResult::fail('Failed to get re2c version', 'build-re2c');
|
||||
}
|
||||
$version_string = $matches[1];
|
||||
if (version_compare($version_string, '1.0.3') < 0) {
|
||||
return CheckResult::fail('re2c version is too low (' . $version_string . ')', 'build-re2c');
|
||||
}
|
||||
return CheckResult::ok($version_string);
|
||||
}
|
||||
|
||||
#[FixItem('build-re2c')]
|
||||
public function buildRe2c(): bool
|
||||
{
|
||||
// TODO: implement re2c build process
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user