This commit is contained in:
crazywhalecc
2025-11-30 15:35:04 +08:00
parent f6c818d3c0
commit 14bfb4198a
179 changed files with 19502 additions and 655 deletions

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\ExceptionHandler;
use StaticPHP\Exception\SPCException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
abstract class BaseCommand extends Command
{
/**
* The message of the day (MOTD) displayed when the command is run.
* You can customize this to show your application's name and version if you are using SPC in vendor mode.
*/
public static string $motd = ' ____ _ _ _ ____ _ _ ____
/ ___|| |_ __ _| |_(_) ___| _ \| | | | _ \
\___ \| __/ _` | __| |/ __| |_) | |_| | |_) |
___) | || (_| | |_| | (__| __/| _ | __/
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| v{version}
';
protected bool $no_motd = false;
protected InputInterface $input;
protected OutputInterface $output;
public function __construct(?string $name = null)
{
parent::__construct($name);
$this->addOption('debug', null, null, '(deprecated) Enable debug mode');
$this->addOption('no-motd', null, null, 'Disable motd');
}
public function initialize(InputInterface $input, OutputInterface $output): void
{
$this->input = $input;
$this->output = $output;
// Bind command context to ApplicationContext
ApplicationContext::bindCommandContext($input, $output);
if ($input->getOption('no-motd')) {
$this->no_motd = true;
}
set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) {
$tips = [
E_WARNING => ['PHP Warning: ', 'warning'],
E_NOTICE => ['PHP Notice: ', 'notice'],
E_USER_ERROR => ['PHP Error: ', 'error'],
E_USER_WARNING => ['PHP Warning: ', 'warning'],
E_USER_NOTICE => ['PHP Notice: ', 'notice'],
E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'],
E_DEPRECATED => ['PHP Deprecated: ', 'notice'],
E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'],
];
$level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error'];
$error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
logger()->{$level_tip[1]}($error);
// 如果 return false 则错误会继续递交给 PHP 标准错误处理
return true;
});
$version = $this->getVersionWithCommit();
if (!$this->no_motd) {
echo str_replace('{version}', $version, self::$motd);
}
}
abstract public function handle(): int;
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
// handle verbose option
$level = match ($this->output->getVerbosity()) {
OutputInterface::VERBOSITY_VERBOSE => 'info',
OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug',
default => 'warning',
};
logger()->setLevel($level);
// ansi
if ($this->input->getOption('no-ansi')) {
logger()->setDecorated(false);
}
// Set debug mode in ApplicationContext
$isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
ApplicationContext::setDebug($isDebug);
// show raw argv list for logger()->debug
logger()->debug('argv: ' . implode(' ', $_SERVER['argv']));
return $this->handle();
} /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) {
// Handle SPCException and log it
ExceptionHandler::handleSPCException($e);
return static::FAILURE;
} catch (\Throwable $e) {
// Handle any other exceptions
ExceptionHandler::handleDefaultException($e);
return static::FAILURE;
}
}
protected function getOption(string $name): mixed
{
return $this->input->getOption($name);
}
protected function getArgument(string $name): mixed
{
return $this->input->getArgument($name);
}
/**
* Get version string with git commit short ID if available.
*/
private function getVersionWithCommit(): string
{
$version = $this->getApplication()->getVersion();
// Don't show commit ID when running in phar
if (\Phar::running()) {
return $version;
}
$commitId = $this->getGitCommitShortId();
if ($commitId) {
return "{$version} ({$commitId})";
}
return $version;
}
/**
* Get git commit short ID without executing git command.
*/
private function getGitCommitShortId(): ?string
{
try {
$gitDir = ROOT_DIR . '/.git';
if (!is_dir($gitDir)) {
return null;
}
$headFile = $gitDir . '/HEAD';
if (!file_exists($headFile)) {
return null;
}
$head = trim(file_get_contents($headFile));
// If HEAD contains 'ref:', it's a branch reference
if (str_starts_with($head, 'ref: ')) {
$ref = substr($head, 5);
$refFile = $gitDir . '/' . $ref;
if (file_exists($refFile)) {
$commit = trim(file_get_contents($refFile));
return substr($commit, 0, 7);
}
} else {
// HEAD contains the commit hash directly (detached HEAD)
return substr($head, 0, 7);
}
} catch (\Throwable) {
// Silently fail if we can't read git info
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
#[AsCommand('build:libs')]
class BuildLibsCommand extends BaseCommand
{
public function configure()
{
$this->addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated');
}
public function handle(): int
{
$libs = parse_comma_list($this->input->getArgument('libraries'));
$installer = new \StaticPHP\Package\PackageInstaller($this->input->getOptions());
foreach ($libs as $lib) {
$installer->addBuildPackage($lib);
}
$installer->run();
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Util\V2CompatLayer;
use Symfony\Component\Console\Input\InputOption;
class BuildTargetCommand extends BaseCommand
{
public function __construct(private readonly string $target, ?string $description = null)
{
parent::__construct("build:{$target}");
if ($target === 'php') {
$this->setAliases(['build']);
}
$this->setDescription($description ?? "Build {$target} target from source");
$pkg = PackageLoader::getTargetPackage($target);
$this->getDefinition()->addOptions($pkg->_exportBuildOptions());
$this->getDefinition()->addArguments($pkg->_exportBuildArguments());
// Builder options
$this->getDefinition()->addOptions([
new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'),
new InputOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional packages to install/build, comma separated', ''),
new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'),
...V2CompatLayer::getLegacyBuildOptions(),
]);
// Downloader options (with 'dl-' prefix to avoid conflicts)
$this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions('dl'));
}
public function handle(): int
{
// resolve legacy options to new options
V2CompatLayer::convertOptions($this->input);
$starttime = microtime(true);
// run installer
$installer = new PackageInstaller($this->input->getOptions());
$installer->addBuildPackage($this->target);
$installer->run();
$usedtime = round(microtime(true) - $starttime, 1);
$this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
$this->output->writeln("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\Doctor\Doctor;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('doctor', 'Diagnose whether the current environment can compile normally')]
class DoctorCommand extends BaseCommand
{
public function configure(): void
{
$this->addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false);
}
public function handle(): int
{
$fix_policy = match ($this->input->getOption('auto-fix')) {
'never' => FIX_POLICY_DIE,
true, null => FIX_POLICY_AUTOFIX,
default => FIX_POLICY_PROMPT,
};
$doctor = new Doctor($this->output, $fix_policy);
if ($doctor->checkAll()) {
$this->output->writeln('<info>Doctor check complete !</info>');
return static::SUCCESS;
}
return static::FAILURE;
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('download')]
class DownloadCommand extends BaseCommand
{
public function configure(): void
{
$this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to download, comma separated, e.g "php-src,openssl,curl"');
// 2.x compatible options
$this->addOption('shallow-clone', null, null, '(deprecated) Clone shallowly repositories when downloading sources');
$this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Fetch by extensions, e.g "openssl,mbstring"');
$this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"');
$this->addOption('without-suggests', null, null, 'Do not fetch suggested sources when using --for-extensions');
// download command specific options
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
$this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Fetch by packages, e.g "php,libssl,libcurl"');
// shared downloader options (no prefix for download command)
$this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions());
}
public function handle(): int
{
// handle --clean option
if ($this->getOption('clean')) {
return $this->handleClean();
}
$downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions()));
// arguments
if ($artifacts = $this->getArgument('artifacts')) {
$artifacts = parse_comma_list($artifacts);
$downloader->addArtifacts($artifacts);
}
// for-extensions
$packages = [];
if ($exts = $this->getOption('for-extensions')) {
$packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts));
// when using for-extensions, also include php package
array_unshift($packages, 'php');
array_unshift($packages, 'php-micro');
array_unshift($packages, 'php-embed');
array_unshift($packages, 'php-fpm');
}
// for-libs / for-packages
if ($libs = $this->getOption('for-libs')) {
$packages = array_merge($packages, parse_comma_list($libs));
}
if ($libs = $this->getOption('for-packages')) {
$packages = array_merge($packages, parse_comma_list($libs));
}
// resolve package dependencies and get artifacts directly
$resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests'));
foreach ($resolved as $pkg_name) {
$pkg = PackageLoader::getPackage($pkg_name);
if ($artifact = $pkg->getArtifact()) {
$downloader->add($artifact);
}
}
$starttime = microtime(true);
$downloader->download();
$endtime = microtime(true);
$elapsed = round($endtime - $starttime);
$this->output->writeln('');
$this->output->writeln('<info>Download completed in ' . $elapsed . ' s.</info>');
return static::SUCCESS;
}
private function handleClean(): int
{
logger()->warning('You are doing some operations that are not recoverable:');
logger()->warning('- Removing directory: ' . SOURCE_PATH);
logger()->warning('- Removing directory: ' . DOWNLOAD_PATH);
logger()->warning('- Removing directory: ' . BUILD_ROOT_PATH);
logger()->alert('I will remove these directories after 5 seconds!');
sleep(5);
if (is_dir(SOURCE_PATH)) {
InteractiveTerm::indicateProgress('Removing: ' . SOURCE_PATH);
FileSystem::removeDir(SOURCE_PATH);
InteractiveTerm::finish('Removed: ' . SOURCE_PATH);
}
if (is_dir(DOWNLOAD_PATH)) {
InteractiveTerm::indicateProgress('Removing: ' . DOWNLOAD_PATH);
FileSystem::removeDir(DOWNLOAD_PATH);
InteractiveTerm::finish('Removed: ' . DOWNLOAD_PATH);
}
if (is_dir(BUILD_ROOT_PATH)) {
InteractiveTerm::indicateProgress('Removing: ' . BUILD_ROOT_PATH);
FileSystem::removeDir(BUILD_ROOT_PATH);
InteractiveTerm::finish('Removed: ' . BUILD_ROOT_PATH);
}
InteractiveTerm::notice('Clean completed.');
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use ZM\Logger\ConsoleColor;
#[AsCommand('extract')]
class ExtractCommand extends BaseCommand
{
public function configure(): void
{
$this->setDescription('Extract downloaded artifacts to their target locations');
$this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to extract, comma separated, e.g "php-src,openssl,curl"');
$this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Extract artifacts for extensions, e.g "openssl,mbstring"');
$this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Extract artifacts for libraries, e.g "libcares,openssl"');
$this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Extract artifacts for packages, e.g "php,libssl,libcurl"');
$this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions');
$this->addOption('force-source', null, null, 'Force extract source even if binary is available');
}
public function handle(): int
{
$cache = ApplicationContext::get(ArtifactCache::class);
$extractor = new ArtifactExtractor($cache);
$force_source = (bool) $this->getOption('force-source');
$artifacts = [];
// Direct artifact names
if ($artifact_arg = $this->getArgument('artifacts')) {
$artifact_names = parse_comma_list($artifact_arg);
foreach ($artifact_names as $name) {
$artifact = ArtifactLoader::getArtifactInstance($name);
if ($artifact === null) {
$this->output->writeln("<error>Artifact '{$name}' not found.</error>");
return static::FAILURE;
}
$artifacts[$name] = $artifact;
}
}
// Resolve packages and get their artifacts
$packages = [];
if ($exts = $this->getOption('for-extensions')) {
$packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts));
// Include php package when using for-extensions
array_unshift($packages, 'php');
}
if ($libs = $this->getOption('for-libs')) {
$packages = array_merge($packages, parse_comma_list($libs));
}
if ($pkgs = $this->getOption('for-packages')) {
$packages = array_merge($packages, parse_comma_list($pkgs));
}
if (!empty($packages)) {
$resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests'));
foreach ($resolved as $pkg_name) {
$pkg = PackageLoader::getPackage($pkg_name);
if ($artifact = $pkg->getArtifact()) {
$artifacts[$artifact->getName()] = $artifact;
}
}
}
if (empty($artifacts)) {
$this->output->writeln('<comment>No artifacts specified. Use artifact names or --for-extensions/--for-libs/--for-packages options.</comment>');
$this->output->writeln('');
$this->output->writeln('Examples:');
$this->output->writeln(' spc extract php-src,openssl');
$this->output->writeln(' spc extract --for-extensions=openssl,mbstring');
$this->output->writeln(' spc extract --for-libs=libcurl,libssl');
return static::SUCCESS;
}
// make php-src always extracted first
uksort($artifacts, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : 0));
try {
InteractiveTerm::notice('Extracting ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . '...');
InteractiveTerm::indicateProgress('Extracting artifacts');
foreach ($artifacts as $artifact) {
InteractiveTerm::setMessage('Extracting artifact: ' . ConsoleColor::green($artifact->getName()));
$extractor->extract($artifact, $force_source);
}
InteractiveTerm::finish('Extracted all artifacts successfully.');
} catch (\Exception $e) {
InteractiveTerm::finish('Extraction failed!', false);
throw $e;
}
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\PackageInstaller;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand('install-pkg')]
class InstallPackageCommand extends BaseCommand
{
public function configure()
{
$this->addArgument('package', null, 'The package to install (name or path)');
}
public function handle(): int
{
ApplicationContext::set('elephant', true);
$installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]);
$installer->addInstallPackage($this->input->getArgument('package'));
$installer->run(true, true);
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command;
use StaticPHP\Util\SPCConfigUtil;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('spc-config', 'Build dependencies')]
class SPCConfigCommand extends BaseCommand
{
protected bool $no_motd = true;
public function configure(): void
{
$this->addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated');
$this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
$this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs');
$this->addOption('with-suggests', null, null, 'Build with suggested packages for selected exts and libs');
$this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts');
$this->addOption('includes', null, null, 'Add additional include path');
$this->addOption('libs', null, null, 'Add additional libs path');
$this->addOption('libs-only-deps', null, null, 'Output dependent libraries with -l prefix');
$this->addOption('absolute-libs', null, null, 'Output absolute paths for libraries');
$this->addOption('no-php', null, null, 'Link to PHP library');
}
public function handle(): int
{
// transform string to array
$libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs'))));
// transform string to array
$extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : [];
$include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts');
$util = new SPCConfigUtil(options: [
'no_php' => $this->getOption('no-php'),
'libs_only_deps' => $this->getOption('libs-only-deps'),
'absolute_libs' => $this->getOption('absolute-libs'),
]);
$packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries);
$config = $util->config($packages, $include_suggests);
$this->output->writeln(match (true) {
$this->getOption('includes') => $config['cflags'],
$this->getOption('libs-only-deps') => $config['libs'],
$this->getOption('libs') => "{$config['ldflags']} {$config['libs']}",
default => "{$config['cflags']} {$config['ldflags']} {$config['libs']}",
});
return 0;
}
}