Add doctor cache check and version management to ensure environment validation

This commit is contained in:
crazywhalecc 2026-02-28 10:32:50 +08:00
parent 7623b9e673
commit c218aef947
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
8 changed files with 94 additions and 0 deletions

3
.gitignore vendored
View File

@ -33,6 +33,9 @@ packlib_files.txt
.php-cs-fixer.cache .php-cs-fixer.cache
.phpunit.result.cache .phpunit.result.cache
# doctor cache fallback (when ~/.cache/spc/ is not writable)
.spc-doctor.lock
# exclude self-runtime # exclude self-runtime
/bin/* /bin/*
!/bin/spc* !/bin/spc*

View File

@ -7,6 +7,7 @@ namespace Package\Target;
use Package\Target\php\frankenphp; use Package\Target\php\frankenphp;
use Package\Target\php\unix; use Package\Target\php\unix;
use Package\Target\php\windows; use Package\Target\php\windows;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\Info;
use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\InitPackage;
@ -104,6 +105,24 @@ class php extends TargetPackage
throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again');
} }
/**
* Get PHP version from source archive filename
*
* @return null|string PHP version (e.g., "8.4.0")
*/
public static function getPHPVersionFromArchive(bool $return_null_if_failed = false): ?string
{
$archives = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src');
$filename = $archives['filename'] ?? '';
if (!preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+|alpha\d+|beta\d+)?)\.tar\.(?:gz|bz2|xz)/', $filename, $match)) {
if ($return_null_if_failed) {
return null;
}
throw new WrongUsageException('PHP source archive filename format is malformed (got: ' . $filename . ')');
}
return $match[1];
}
#[InitPackage] #[InitPackage]
public function init(TargetPackage $package): void public function init(TargetPackage $package): void
{ {
@ -255,6 +274,7 @@ class php extends TargetPackage
'Build Target' => getenv('SPC_TARGET') ?: '', 'Build Target' => getenv('SPC_TARGET') ?: '',
'Build Toolchain' => ToolchainManager::getToolchainClass(), 'Build Toolchain' => ToolchainManager::getToolchainClass(),
'Build SAPI' => implode(', ', $sapis), 'Build SAPI' => implode(', ', $sapis),
'PHP Version' => self::getPHPVersion(return_null_if_failed: true) ?? self::getPHPVersionFromArchive(return_null_if_failed: true) ?? 'Unknown',
'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)),
'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions),
'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)),

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace StaticPHP\Command; namespace StaticPHP\Command;
use StaticPHP\DI\ApplicationContext; use StaticPHP\DI\ApplicationContext;
use StaticPHP\Doctor\Doctor;
use StaticPHP\Exception\ExceptionHandler; use StaticPHP\Exception\ExceptionHandler;
use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCException;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -118,6 +119,21 @@ abstract class BaseCommand extends Command
} }
} }
/**
* Warn the user if doctor has not been run (or is outdated).
* Set SPC_SKIP_DOCTOR_CHECK=1 to suppress.
*/
protected function checkDoctorCache(): void
{
if (getenv('SPC_SKIP_DOCTOR_CHECK') || Doctor::isHealthy()) {
return;
}
$this->output->writeln('');
$this->output->writeln('<comment>[WARNING] Please run `spc doctor` first to verify your build environment.</comment>');
$this->output->writeln('');
sleep(2);
}
protected function getOption(string $name): mixed protected function getOption(string $name): mixed
{ {
return $this->input->getOption($name); return $this->input->getOption($name);

View File

@ -44,6 +44,8 @@ class BuildLibsCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
$this->checkDoctorCache();
$libs = parse_comma_list($this->input->getArgument('libraries')); $libs = parse_comma_list($this->input->getArgument('libraries'));
$installer = new PackageInstaller($this->input->getOptions()); $installer = new PackageInstaller($this->input->getOptions());

View File

@ -37,6 +37,8 @@ class BuildTargetCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
$this->checkDoctorCache();
// resolve legacy options to new options // resolve legacy options to new options
V2CompatLayer::convertOptions($this->input); V2CompatLayer::convertOptions($this->input);

View File

@ -26,6 +26,7 @@ class DoctorCommand extends BaseCommand
}; };
$doctor = new Doctor($this->output, $fix_policy); $doctor = new Doctor($this->output, $fix_policy);
if ($doctor->checkAll()) { if ($doctor->checkAll()) {
Doctor::markPassed();
$this->output->writeln('<info>Doctor check complete !</info>'); $this->output->writeln('<info>Doctor check complete !</info>');
return static::SUCCESS; return static::SUCCESS;
} }

View File

@ -56,6 +56,8 @@ class DownloadCommand extends BaseCommand
return $this->handleClean(); return $this->handleClean();
} }
$this->checkDoctorCache();
$downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions()));
// arguments // arguments

View File

@ -9,6 +9,7 @@ use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException; use StaticPHP\Exception\SPCException;
use StaticPHP\Registry\DoctorLoader; use StaticPHP\Registry\DoctorLoader;
use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor; use ZM\Logger\ConsoleColor;
@ -25,6 +26,29 @@ readonly class Doctor
logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names));
} }
/**
* Returns true if doctor was previously passed with the current SPC version.
*/
public static function isHealthy(): bool
{
$lock = self::getLockPath();
return file_exists($lock) && trim((string) @file_get_contents($lock)) === \StaticPHP\ConsoleApplication::VERSION;
}
/**
* Write current SPC version to the lock file, marking doctor as passed.
*/
public static function markPassed(): void
{
$primary = self::getLockPath();
if (!is_dir(dirname($primary))) {
@mkdir(dirname($primary), 0755, true);
}
if (@file_put_contents($primary, \StaticPHP\ConsoleApplication::VERSION) === false) {
@file_put_contents((getcwd() ?: '.') . DIRECTORY_SEPARATOR . '.spc-doctor.lock', \StaticPHP\ConsoleApplication::VERSION);
}
}
/** /**
* Check all valid check items. * Check all valid check items.
* @return bool true if all checks passed, false otherwise * @return bool true if all checks passed, false otherwise
@ -119,6 +143,30 @@ readonly class Doctor
return false; return false;
} }
private static function getLockPath(): string
{
if (SystemTarget::getTargetOS() === 'Windows') {
$trial_ls = [
getenv('LOCALAPPDATA') ?: ((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock',
sys_get_temp_dir() . '\.spc-doctor.lock',
WORKING_DIR . '\.spc-doctor.lock',
];
} else {
$trial_ls = [
getenv('XDG_CACHE_HOME') ?: ((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock',
sys_get_temp_dir() . '/.spc-doctor.lock',
WORKING_DIR . '/.spc-doctor.lock',
];
}
foreach ($trial_ls as $path) {
if (is_writable(dirname($path))) {
return $path;
}
}
// fallback to current directory
return WORKING_DIR . DIRECTORY_SEPARATOR . '.spc-doctor.lock';
}
private function emitFix(string $fix_item, array $fix_item_params = []): bool private function emitFix(string $fix_item, array $fix_item_params = []): bool
{ {
keyboard_interrupt_register(function () { keyboard_interrupt_register(function () {