From 2db7716cc3ddcc702d7a32135ebdb334741efe57 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Sun, 16 Mar 2025 18:59:04 +0800 Subject: [PATCH 1/6] feature phpy --- .gitignore | 4 +- bin/phpy | 12 +- composer.json | 3 +- tools/src/Commands/AbstractCommand.php | 223 -------------- tools/src/Phpy/Application.php | 216 ++++++++++++++ tools/src/Phpy/Commands/AbstractCommand.php | 71 +++++ tools/src/Phpy/Commands/InstallCommand.php | 108 +++++++ tools/src/{ => Phpy}/Commands/PhpyInstall.php | 21 +- .../{ => Phpy}/Commands/PipMirrorConfig.php | 2 +- .../{ => Phpy}/Commands/PipModuleInstall.php | 4 +- .../src/{ => Phpy}/Commands/PythonInstall.php | 2 +- tools/src/Phpy/Commands/ShowCommand.php | 49 +++ tools/src/Phpy/Commands/UpdateCommand.php | 91 ++++++ tools/src/Phpy/Config.php | 107 +++++++ tools/src/Phpy/ConsoleIO.php | 197 +++++++++++++ .../Exceptions/CommandFailedException.php | 10 + .../Phpy/Exceptions/CommandStopException.php | 10 + .../Exceptions/CommandSuccessedException.php | 10 + tools/src/Phpy/Exceptions/PhpyException.php | 10 + tools/src/Phpy/Helpers/Process.php | 155 ++++++++++ tools/src/Phpy/Helpers/System.php | 250 ++++++++++++++++ .../Phpy/Installers/BuildToolsInstaller.php | 83 ++++++ .../Phpy/Installers/InstallerInterface.php | 48 +++ tools/src/Phpy/Installers/ModuleInstaller.php | 279 ++++++++++++++++++ tools/src/Phpy/Installers/PhpyInstaller.php | 115 ++++++++ tools/src/Phpy/Installers/PythonInstaller.php | 204 +++++++++++++ 26 files changed, 2035 insertions(+), 249 deletions(-) delete mode 100644 tools/src/Commands/AbstractCommand.php create mode 100644 tools/src/Phpy/Application.php create mode 100644 tools/src/Phpy/Commands/AbstractCommand.php create mode 100644 tools/src/Phpy/Commands/InstallCommand.php rename tools/src/{ => Phpy}/Commands/PhpyInstall.php (87%) rename tools/src/{ => Phpy}/Commands/PipMirrorConfig.php (98%) rename tools/src/{ => Phpy}/Commands/PipModuleInstall.php (96%) rename tools/src/{ => Phpy}/Commands/PythonInstall.php (99%) create mode 100644 tools/src/Phpy/Commands/ShowCommand.php create mode 100644 tools/src/Phpy/Commands/UpdateCommand.php create mode 100644 tools/src/Phpy/Config.php create mode 100644 tools/src/Phpy/ConsoleIO.php create mode 100644 tools/src/Phpy/Exceptions/CommandFailedException.php create mode 100644 tools/src/Phpy/Exceptions/CommandStopException.php create mode 100644 tools/src/Phpy/Exceptions/CommandSuccessedException.php create mode 100644 tools/src/Phpy/Exceptions/PhpyException.php create mode 100644 tools/src/Phpy/Helpers/Process.php create mode 100644 tools/src/Phpy/Helpers/System.php create mode 100644 tools/src/Phpy/Installers/BuildToolsInstaller.php create mode 100644 tools/src/Phpy/Installers/InstallerInterface.php create mode 100644 tools/src/Phpy/Installers/ModuleInstaller.php create mode 100644 tools/src/Phpy/Installers/PhpyInstaller.php create mode 100644 tools/src/Phpy/Installers/PythonInstaller.php diff --git a/.gitignore b/.gitignore index fc4d358..c7e107a 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,6 @@ acinclude.m4 config.guess config.sub /composer.lock - +/*.command +/py-vendor +/requirements.txt diff --git a/bin/phpy b/bin/phpy index f3631a8..4a0decd 100755 --- a/bin/phpy +++ b/bin/phpy @@ -1,20 +1,12 @@ #!/usr/bin/env php addCommands([ - new \PhpyTool\Commands\PipModuleInstall(), - new \PhpyTool\Commands\PhpyInstall(), - new \PhpyTool\Commands\PythonInstall(), - new \PhpyTool\Commands\PipMirrorConfig(), -]); try { - $application->run(); + (new Application())->run(); } catch (Throwable $e) { exit($e->getMessage() . PHP_EOL); } diff --git a/composer.json b/composer.json index 6ef0631..aaa6b93 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require-dev": { "phpunit/phpunit": "^10.4", - "friendsofphp/php-cs-fixer": "^3.40" + "friendsofphp/php-cs-fixer": "^3.40", + "symfony/var-dumper": "^7.0" }, "autoload": { "psr-4": { diff --git a/tools/src/Commands/AbstractCommand.php b/tools/src/Commands/AbstractCommand.php deleted file mode 100644 index 502556b..0000000 --- a/tools/src/Commands/AbstractCommand.php +++ /dev/null @@ -1,223 +0,0 @@ -addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode.'); - } - - /** - * @return OutputInterface|null - */ - public function getOutput(): ?OutputInterface - { - return $this->output; - } - - /** - * @return InputInterface|null - */ - public function getInput(): ?InputInterface - { - return $this->input; - } - - /** - * @return string|null - */ - public function getRuntimeId(): ?string - { - return $this->runtimeId; - } - - /** - * @param string $info - * @return void - */ - private function debugMode(string $info): void - { - if (self::$debug) { - $this->subOutput($info = "DEBUG INFO -> $info"); - $file = str_replace('\\', '_', get_called_class()) . '-' . $this->getRuntimeId(); - if (!is_dir($dir = getcwd() . "/.log")) { - mkdir($dir, 0777, true); - } - file_put_contents("$dir/$file.log", "$info\n", FILE_APPEND | LOCK_EX); - } - } - - /** - * 执行命令 - * - * @param string $command - * @param mixed|null $output - * @param int|null $resultCode - * @param bool $ignore 忽略中断 - * @return string|int|bool|null - */ - protected function exec(string $command, mixed &$output = null, mixed &$resultCode = 0, bool $ignore = false): string|int|bool|null - { - $this->debugMode("exec( $command )"); - $lastLine = exec($command, $output, $resultCode); - if ($resultCode !== 0 and !$ignore) { - $this->error($lastLine); - return $resultCode; - } - $this->debugMode("->> rc: $resultCode | last info: $lastLine"); - return $lastLine; - } - - /** - * @param string $command - * @param int|null $resultCode - * @param bool $ignore - * @return string|int|bool|null - */ - protected function system(string $command, ?int &$resultCode = 0, bool $ignore = false): string|int|bool|null - { - $this->debugMode("system( $command )"); - $info = system($command, $resultCode); - if ($resultCode !== 0) { - if (!$ignore) { - $this->error($info); - return $resultCode; - } - } - $this->debugMode("->> rc: $resultCode | last info: $info"); - return $info; - } - - /** - * @param string $command - * @param string|null $lastLine - * @return int resultCode - */ - protected function execWithProgress(string $command, ?string &$lastLine = null): int - { - $this->debugMode("execWithProgress( $command )"); - $process = popen($command, 'r'); - while (!feof($process)) { - $line = fgets($process); - if ($line === false) { - break; - } else { - $lastLine = trim($line); - if ($lastLine) { - $this->subOutput($lastLine); - } - } - usleep(1000); - } - $this->debugMode("> rc: null | last info: $lastLine"); - return pclose($process); - } - - /** - * sub输出 - * - * @param string $message - * @param string $tag - * @return void - */ - protected function subOutput(string $message, string $tag = '[>]'): void - { - $this->getOutput()?->getFormatter()->setStyle('sub-output', new OutputFormatterStyle('gray', null, ['underscore'])); - $this->getOutput()?->writeln("$tag $message"); - } - - /** - * 普通输出 - * - * @param string $message - * @param string $tag - * @return void - */ - protected function output(string $message, string $tag = '[>]'): void - { - $this->getOutput()?->writeln("$tag $message"); - } - - /** - * 输出info - * - * @param string $message - * @return void - */ - protected function comment(string $message): void - { - $this->getOutput()?->writeln("[i] $message"); - } - - /** - * 输出error - * - * @param string $message - * @return int - */ - protected function error(string $message): int - { - $this->getOutput()?->writeln("[×] $message"); - return self::FAILURE; - } - - /** - * 输出success - * - * @param string $message - * @return int - */ - protected function success(string $message): int - { - $this->getOutput()?->writeln("[√] $message"); - return self::SUCCESS; - } - - /** @inheritdoc */ - final protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->runtimeId = uniqid(); - $this->input = $input; - $this->output = $output; - if (self::$debug = $input->getOption('debug')) { - $this->comment("Run in debug mode. ID: {$this->getRuntimeId()}"); - } - return $this->handler(); - } - - /** - * @return int - */ - abstract protected function handler(): int; - -} diff --git a/tools/src/Phpy/Application.php b/tools/src/Phpy/Application.php new file mode 100644 index 0000000..eba885e --- /dev/null +++ b/tools/src/Phpy/Application.php @@ -0,0 +1,216 @@ + _____ + | _ || | || _ | _ _ + | __|| || __|| | | + |__| |__|__||__| |_ | + |___| by Swoole +doc; + + public function __construct() + { + System::setcwd(getcwd()); + parent::__construct('PHPy', static::VERSION); + $this->addCommands([ + new InstallCommand(), + new UpdateCommand(), + new ShowCommand(), + new PipModuleInstall(), + new PhpyInstall(), + new PythonInstall(), + new PipMirrorConfig(), + ]); + } + + /** + * @return string + */ + public function getLogo(): string + { + return $this->logo; + } + + /** @inheritdoc */ + public function getLongVersion(): string + { + return \sprintf(<<%s version %s +EOT, $this->getLogo(), $this->getName(), $this->getVersion()); + } + + /** + * 获取配置文件 + * + * @param string $dir + * @return string|null + */ + public static function getConfigFile(string $dir): ?string + { + $filePath = "$dir/phpy.json"; + return file_exists($filePath) ? $filePath : null; + } + + /** + * 设置配置文件 + * + * @param string $dir + * @param array $data + * @return bool|int + */ + public static function setConfigFile(string $dir, array $data): bool|int + { + $filePath = "$dir/phpy.json"; + if (file_exists($filePath)) { + return false; + } + return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE)); + } + + /** + * 获取锁定文件 + * + * @param string $dir + * @return string|null + */ + public static function getLockFile(string $dir): ?string + { + $filePath = "$dir/phpy.lock"; + return file_exists($filePath) ? $filePath : null; + } + + /** + * 设置锁定文件 + * + * @param string $dir + * @param array $data + * @return bool|int + */ + public static function setLockFile(string $dir, array $data): bool|int + { + $filePath = "$dir/phpy.lock"; + if (file_exists($filePath) or !isset($data['hash'])) { + return false; + } + return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE)); + } + + /** + * 获取锁定文件 + * + * @param string $dir + * @return string|null + */ + public static function getRequirementsFile(string $dir): ?string + { + $filePath = "$dir/requirements.txt"; + return file_exists($filePath) ? $filePath : null; + } + + /** + * 递归向上查找配置文件 + * + * @param Closure|null $closure + * @return string|null + */ + public static function findConfigFile(?Closure $closure = null): string|null + { + $currentDir = $startDir = System::getcwd(); + while ($currentDir !== dirname($currentDir)) {dump(1); + if ($filePath = static::getConfigFile($currentDir)) { + return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + } + $currentDir = dirname($currentDir); + } + return null; + } + + /** + * 递归向上查找锁定文件 + * + * @param Closure|null $closure + * @return string|null + */ + public static function findLockFile(?Closure $closure = null): ?string + { + $currentDir = $startDir = System::getcwd(); + while ($currentDir !== dirname($currentDir)) { + if ($filePath = static::getLockFile($currentDir)) { + return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + } + $currentDir = dirname($currentDir); + } + return null; + } + + /** + * 递归向上查找requirements.txt文件 + * + * @param Closure|null $closure + * @return string|null + */ + public static function findRequirementsFile(?Closure $closure = null): ?string + { + $currentDir = $startDir = System::getcwd(); + while ($currentDir !== dirname($currentDir)) { + if ($filePath = static::getRequirementsFile($currentDir)) { + return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + } + $currentDir = dirname($currentDir); + } + return null; + } + + /** + * 获取所有配置文件树 + * + * @param Closure|null $closure = function ($organization, $package, $configFilePath) {} + * @return array>|null + */ + public static function getVendorConfigFiles(?Closure $closure = null): ?array + { + if (!is_dir($vendorDir = System::getcwd() . DIRECTORY_SEPARATOR . 'vendor')) { + return null; + } + + $configFilesTree = []; + $organizationDirs = array_filter(glob($vendorDir . '/*'), 'is_dir'); + + foreach ($organizationDirs as $organizationDir) { + $organization = basename($organizationDir); + $packageDirs = array_filter(glob($organizationDir . '/*'), 'is_dir'); + + foreach ($packageDirs as $packageDir) { + $package = basename($packageDir); + $configFilePath = "$packageDir/phpy.json"; + + if (file_exists($configFilePath)) { + $configFilesTree[$organization][$package] = $closure ? call_user_func($closure, $organization, $package, $configFilePath) : $configFilePath; + } + } + } + + return $configFilesTree; + } +} diff --git a/tools/src/Phpy/Commands/AbstractCommand.php b/tools/src/Phpy/Commands/AbstractCommand.php new file mode 100644 index 0000000..6549829 --- /dev/null +++ b/tools/src/Phpy/Commands/AbstractCommand.php @@ -0,0 +1,71 @@ +addOption('debug', null, InputOption::VALUE_NONE, 'Enable debug mode.'); + } + + /** + * @return string|null + */ + public function getRuntimeId(): ?string + { + return $this->process?->getRuntimeId(); + } + + /** + * @return Process|null + */ + public function getProcess(): ?Process + { + return $this->process; + } + + /** + * @return ConsoleIO|null + */ + public function getConsoleIO(): ?ConsoleIO + { + return $this->consoleIO; + } + + /** @inheritdoc */ + final protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->consoleIO = new ConsoleIO($input, $output, $this->getHelperSet()); + $this->process = new Process($this->consoleIO, uniqid(), (bool)$input->getOption('debug')); + return $this->handler(); + } + + /** + * @return int + */ + abstract protected function handler(): int; + +} diff --git a/tools/src/Phpy/Commands/InstallCommand.php b/tools/src/Phpy/Commands/InstallCommand.php new file mode 100644 index 0000000..22aa930 --- /dev/null +++ b/tools/src/Phpy/Commands/InstallCommand.php @@ -0,0 +1,108 @@ +setName('install') + ->setDescription('Installs the project dependencies from the phpy.lock file if present, or falls back on the phpy.json') + ->addOption('skip-env', null, null, 'Skip the environment installation.') + ->addOption('skip-ext', null, null, 'Skip the phpy extension installation.') + ->addOption('skip-module', null, null, 'Skip the module installation.') + ->setHelp( + <<install command checks and installs the Python +environment based on phpy.json, with methods like conda or venv +configurable through the Python module in php.json. +Use --skip-env to skip this step. + +It also checks and installs phpy extensions based on phpy.json. +The phpy version, automatic injection into php.ini, and other +configurations can be set through the phpy module in php.json. +Use --skip-ext to skip this step. + +Finally, the command reads phpy.lock from the current +directory to install Python module dependencies, using the +environment specified in phpy.json to execute pip install. +Use --skip-module to skip this step. + +PHPy introduces modules through Python-pip, read more at +https://pypi.org/help/ +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + try { + // find json file + $jsonFile = Application::findConfigFile(function ($file, $cDir, $sDir) { + if ($cDir !== $sDir) { + if (!$this->consoleIO?->ask( + "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", + true, + ConfirmationQuestion::class + )) { + throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); + } + } + System::setcwd($cDir); + }); + if (!$jsonFile) { + throw new CommandFailedException('PHPy could not find a phpy.json file in the project'); + } + // 尝试读取lock + if (!$lockFile = Application::getLockFile(System::getcwd())) { + $this->consoleIO?->comment("No phpy.lock file present. Updating dependencies to latest instead of installing from lock file."); + } + $config = new Config($lockFile ?: $jsonFile); + // install python env + if (!$this->consoleIO?->getInput()->getOption('skip-env')) { + (new PythonInstaller($config, $this->consoleIO))->install(); + } + // install phpy extensions + if (!$this->consoleIO?->getInput()->getOption('skip-ext')) { + (new PhpyInstaller($config, $this->consoleIO))->install(); + } + // install modules + if (!$this->consoleIO?->getInput()->getOption('skip-module')) { + // build tools + (new BuildToolsInstaller($config, $this->consoleIO))->install(); + // module + (new ModuleInstaller($config, $this->consoleIO))->install(); + } + if (!$lockFile) { + $config->set('hash', hash_file('SHA-256', $jsonFile)); + Application::setLockFile(System::getcwd(), $config->all()); + } + } catch (CommandStopException) { + return $this->consoleIO?->success('Installation stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); + } + return $this->consoleIO?->error('Installation failed.'); + } +} diff --git a/tools/src/Commands/PhpyInstall.php b/tools/src/Phpy/Commands/PhpyInstall.php similarity index 87% rename from tools/src/Commands/PhpyInstall.php rename to tools/src/Phpy/Commands/PhpyInstall.php index e6ee385..f2f323f 100644 --- a/tools/src/Commands/PhpyInstall.php +++ b/tools/src/Phpy/Commands/PhpyInstall.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpyTool\Commands; +namespace PhpyTool\Phpy\Commands; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; @@ -25,33 +25,34 @@ protected function configure(): void protected function handler(): int { $helper = new QuestionHelper(); - $version = $this->getInput()?->getArgument('version'); + $version = $this->consoleIO?->getInput()->getArgument('version'); // 询问安装目录 - $question = new Question("[?] Please specify the installation directory (default: .runtime): \n", getcwd() . '/.runtime'); - $installDir = $helper->ask($this->getInput(), $this->getOutput(), $question) . '/swoole_phpy_' . str_replace('.', '', $version); + $installDir = $this->consoleIO + ?->ask('Please specify the installation directory (default: .runtime):', getcwd() . '/.runtime') + . '/swoole_phpy_' . str_replace('.', '', $version); if (!file_exists($installDir)) { // 下载源码 - $this->output('Downloading the latest source code ...'); + $this->consoleIO?->output('Downloading the latest source code ...'); if ( $this->execWithProgress($version === 'latest' ? "git clone --depth 1 https://github.com/swoole/phpy.git $installDir" : "git clone --depth 1 --branch $version https://github.com/swoole/phpy.git $installDir") !== 0 ) { - return $this->error('Error downloading source code.'); + return $this->consoleIO?->error('Error downloading source code.'); } } else { - $this->comment('PHPy source code already downloaded.'); + $this->consoleIO?->comment('PHPy source code already downloaded.'); } // 安装编译依赖组件 - $this->output('Installing dependencies...'); + $this->consoleIO?->output('Installing dependencies...'); if ($installCommands = $this->getSystemInstallCommands()) { if ($this->execWithProgress($installCommands) !== 0) { - return $this->error('Error installing dependencies.'); + return $this->consoleIO?->error('Error installing dependencies.'); } } else { - return $this->error('Please install PHPy manually.'); + return $this->consoleIO?->error('Please install PHPy manually.'); } $question = new Question("[?] Please specify the Python-config directory (default: /usr/bin/python-config): \n", '/usr/bin/python-config'); diff --git a/tools/src/Commands/PipMirrorConfig.php b/tools/src/Phpy/Commands/PipMirrorConfig.php similarity index 98% rename from tools/src/Commands/PipMirrorConfig.php rename to tools/src/Phpy/Commands/PipMirrorConfig.php index 55f83df..3a4bea6 100644 --- a/tools/src/Commands/PipMirrorConfig.php +++ b/tools/src/Phpy/Commands/PipMirrorConfig.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpyTool\Commands; +namespace PhpyTool\Phpy\Commands; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Question\ChoiceQuestion; diff --git a/tools/src/Commands/PipModuleInstall.php b/tools/src/Phpy/Commands/PipModuleInstall.php similarity index 96% rename from tools/src/Commands/PipModuleInstall.php rename to tools/src/Phpy/Commands/PipModuleInstall.php index 8fa12b3..7d3a3e7 100644 --- a/tools/src/Commands/PipModuleInstall.php +++ b/tools/src/Phpy/Commands/PipModuleInstall.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpyTool\Commands; +namespace PhpyTool\Phpy\Commands; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; @@ -18,7 +18,7 @@ protected function configure(): void parent::configure(); $this ->setName('install:pip-module') - ->setDescription('Installs Python PyORC module.') + ->setDescription('Installs Python module.') ->addArgument('module', InputArgument::REQUIRED, 'The module name to install') ->addArgument('version', InputArgument::OPTIONAL, 'The version of Python module to install', 'latest'); } diff --git a/tools/src/Commands/PythonInstall.php b/tools/src/Phpy/Commands/PythonInstall.php similarity index 99% rename from tools/src/Commands/PythonInstall.php rename to tools/src/Phpy/Commands/PythonInstall.php index fde61cc..6edb644 100644 --- a/tools/src/Commands/PythonInstall.php +++ b/tools/src/Phpy/Commands/PythonInstall.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpyTool\Commands; +namespace PhpyTool\Phpy\Commands; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputOption; diff --git a/tools/src/Phpy/Commands/ShowCommand.php b/tools/src/Phpy/Commands/ShowCommand.php new file mode 100644 index 0000000..4e46981 --- /dev/null +++ b/tools/src/Phpy/Commands/ShowCommand.php @@ -0,0 +1,49 @@ +setName('show') + ->addArgument('module', InputArgument::OPTIONAL, 'The module name') + ->setDescription('Shows information about python modules and python-env') + ->setHelp( + <<show command displays detailed information about Python-env and a Python modules, or +lists all Python modules available. + +PHPy introduces modules through Python-pip, read more at +https://pypi.org/help/ +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + $this->consoleIO->output('Python-env: '); + $this->process->pythonExec('--version'); + $this->process->pipExec('--version'); + $this->consoleIO->output('Python-includes: '); + $this->process->pythonConfigExec('--includes'); + + if ($module = $this->consoleIO->getInput()->getArgument('module')) { + $this->consoleIO->output("Python-module [$module]: "); + $this->process->pipExec("show $module"); + } else { + $this->consoleIO->output('Python-modules: '); + $this->process->pipExec('list'); + } + return 0; + } +} diff --git a/tools/src/Phpy/Commands/UpdateCommand.php b/tools/src/Phpy/Commands/UpdateCommand.php new file mode 100644 index 0000000..72f5bf7 --- /dev/null +++ b/tools/src/Phpy/Commands/UpdateCommand.php @@ -0,0 +1,91 @@ +setName('update') + ->setDescription('Updates your dependencies to the latest version according to phpy.json, and updates the phpy.lock file') + ->addArgument('module', InputArgument::OPTIONAL, 'Python module name to update') + ->setHelp( + <<update command reads the phpy.json file from the +current directory, processes it, and updates, removes or installs all the +dependencies. + +PHPy introduces modules through Python-pip, read more at +https://pypi.org/help/ +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + try { + // find json file + $jsonFile = Application::findConfigFile(function ($file, $cDir, $sDir) { + if ($cDir !== $sDir) { + if (!$this->consoleIO?->ask( + "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", + true, + ConfirmationQuestion::class + )) { + throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); + } + } + System::setcwd($cDir); + }); + if (!$jsonFile) { + $sDir = System::getcwd(); + throw new CommandFailedException('PHPy could not find a phpy.json file in the project'); + } + $config = new Config($jsonFile); + // update python env + if (!$this->consoleIO?->getInput()->getOption('skip-env')) { + (new PythonInstaller($config, $this->consoleIO))->upgrade(); + } + // install phpy extensions + if (!$this->consoleIO?->getInput()->getOption('skip-ext')) { + (new PhpyInstaller($config, $this->consoleIO))->upgrade(); + } + // install modules + if (!$this->consoleIO?->getInput()->getOption('skip-module')) { + // build tools + (new BuildToolsInstaller($config, $this->consoleIO))->upgrade(); + // module + (new ModuleInstaller($config, $this->consoleIO))->upgrade(); + } + $config->set('hash', hash_file('SHA-256', $jsonFile)); + Application::setLockFile(System::getcwd(), $config->all()); + } catch (CommandStopException) { + return $this->consoleIO?->success('Installation stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); + } + return $this->consoleIO?->error('Installation failed.'); + } +} diff --git a/tools/src/Phpy/Config.php b/tools/src/Phpy/Config.php new file mode 100644 index 0000000..30286a6 --- /dev/null +++ b/tools/src/Phpy/Config.php @@ -0,0 +1,107 @@ + [ + 'cache-dir' => '~/.cache/phpy', + 'pip-index-url' => '' + ], + 'python' => [ + 'source-url' => 'https://github.com/python/cpython.git', + 'install-dir' => '/usr', + 'install-version' => 'latest', + 'install-configure' => [ + '--enable-optimizations', + '--with-lto', + '--enable-static' + ], + ], + 'phpy' => [ + 'source-url' => 'https://github.com/swoole/phpy.git', + 'install-version' => 'latest', + 'install-configure' => [], + 'ini-path' => '/usr/local/etc/php/conf.d/xx-php-ext-phpy.ini' + ], + 'modules' => [] + ]; + + /** + * @param string|null $file + */ + public function __construct(?string $file = null) + { + $config = []; + if ($file) { + try { + $config = json_decode(System::getFileContent($file), true, flags: JSON_THROW_ON_ERROR); + } catch (\Throwable) {} + } + $this->config = array_replace_recursive($this->config, $config); + } + + /** + * 获取配置 + * + * @param string|null $key + * @param mixed|null $default + * @return mixed + */ + public function get(?string $key, mixed $default = null): mixed + { + $config = $this->config; + if ($key) { + $keyArray = explode('.', $key); + $found = true; + foreach ($keyArray as $index) { + if (!isset($config[$index])) { + $found = false; + break; + } + $config = $config[$index]; + } + + return $found ? $config : $default; + } + + return $config; + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + public function set(string $key, mixed $value): void + { + $keys = explode('.', $key); + $config = &$this->config; + + foreach ($keys as $k) { + if (!isset($config[$k]) or !is_array($config[$k])) { + $config[$k] = []; + } + $config = &$config[$k]; + } + + $config = $value; + } + + /** + * @return array + */ + public function all(): array + { + return $this->config; + } +} diff --git a/tools/src/Phpy/ConsoleIO.php b/tools/src/Phpy/ConsoleIO.php new file mode 100644 index 0000000..750b556 --- /dev/null +++ b/tools/src/Phpy/ConsoleIO.php @@ -0,0 +1,197 @@ +input = $input; + $this->output = $output; + $this->helperSet = $helperSet; + $this->extra = $extra; + } + + /** + * 获取input + * + * @return InputInterface + */ + public function getInput(): InputInterface + { + return $this->input; + } + + /** + * 获取output + * + * @return OutputInterface + */ + public function getOutput(): OutputInterface + { + return $this->output; + } + + /** + * 获取helperSet + * + * @return HelperSet + */ + public function getHelperSet(): HelperSet + { + return $this->helperSet; + } + + /** + * @param string $key + * @param mixed $value + * @return void + */ + public function setExtra(string $key, mixed $value): void + { + $keys = explode('.', $key); + $extra = &$this->extra; + + foreach ($keys as $k) { + if (!isset($extra[$k]) or !is_array($extra[$k])) { + $extra[$k] = []; + } + $extra = &$extra[$k]; + } + + $extra = $value; + } + + /** + * 获取extra + * + * @param string|null $key + * @param mixed|null $default + * @return mixed + */ + public function getExtra(?string $key = null, mixed $default = null): mixed + { + $extra = $this->extra; + if ($key) { + $keyArray = explode('.', $key); + $found = true; + foreach ($keyArray as $index) { + if (!isset($extra[$index])) { + $found = false; + break; + } + $extra = $extra[$index]; + } + + return $found ? $extra : $default; + } + + return $extra; + } + + /** + * @param string $message + * @param mixed|null $default + * @param string $tag + * @param class-string $questionClass + * @return mixed + */ + public function ask(string $message, mixed $default = null, string $tag = '[?]', string $questionClass = Question::class): mixed + { + // 询问安装目录 + $question = new $questionClass("$tag $message \n", $default); + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelperSet()->get('question'); + return $questionHelper->ask($this->getInput(), $this->getOutput(), $question); + } + + /** + * sub输出 + * + * @param string $message + * @param string $tag + * @return void + */ + public function subOutput(string $message, string $tag = '[>]'): void + { + $this->getOutput() + ->getFormatter() + ->setStyle('sub-output', new OutputFormatterStyle('gray', null)); + $this->getOutput() + ->writeln("$tag $message"); + } + + /** + * 普通输出 + * + * @param string $message + * @param string $tag + * @return void + */ + public function output(string $message, string $tag = '[>]'): void + { + $this->getOutput()->writeln("$tag $message"); + } + + /** + * 输出info + * + * @param string $message + * @return void + */ + public function comment(string $message): void + { + $this->getOutput()->writeln("[i] $message"); + } + + /** + * 输出error + * + * @param string $message + * @return int + */ + public function error(string $message): int + { + $this->getOutput()->writeln("[×] $message"); + return Command::FAILURE; + } + + /** + * 输出success + * + * @param string $message + * @return int + */ + public function success(string $message): int + { + $this->getOutput()->writeln("[√] $message"); + return Command::SUCCESS; + } +} diff --git a/tools/src/Phpy/Exceptions/CommandFailedException.php b/tools/src/Phpy/Exceptions/CommandFailedException.php new file mode 100644 index 0000000..ce92e7b --- /dev/null +++ b/tools/src/Phpy/Exceptions/CommandFailedException.php @@ -0,0 +1,10 @@ +runtimeId = $runtimeId ?: uniqid(); + $this->consoleIO = $consoleIO; + $this->debug = $debug; + } + + /** + * @return string + */ + public function getRuntimeId(): string + { + return $this->runtimeId; + } + + /** + * @param string $info + * @return void + */ + private function debugMode(string $info): void + { + if ($this->debug) { + $this->consoleIO?->subOutput($info = "DEBUG INFO -> $info"); + $file = str_replace('\\', '_', get_called_class()) . '-' . $this->runtimeId; + if (!is_dir($dir = getcwd() . "/.log")) { + mkdir($dir, 0777, true); + } + file_put_contents("$dir/$file.log", "$info\n", FILE_APPEND | LOCK_EX); + } + } + + /** + * @param string $command + * @param null|mixed $lastLine + * @return string|int|bool|null + */ + public function pipExec(string $command, mixed &$lastLine = null): string|int|bool|null + { + $pip = System::pip(); + return $this->execWithProgress("$pip $command", $lastLine); + } + + /** + * @param string $command + * @param null|mixed $lastLine + * @return bool|int|string|null + */ + public function pythonExec(string $command, mixed &$lastLine = null): bool|int|string|null + { + $python = System::python(); + return $this->execWithProgress("$python $command", $lastLine); + } + + /** + * @param string $command + * @param null|mixed $lastLine + * @return bool|int|string|null + */ + public function pythonConfigExec(string $command, mixed &$lastLine = null): bool|int|string|null + { + $python = System::pythonConfig(); + return $this->execWithProgress("$python $command", $lastLine); + } + + /** + * 执行命令 + * + * @param string $command + * @param mixed|null $output + * @param int|null $resultCode + * @param bool $ignore 忽略中断 + * @return string|int|bool|null + */ + public function exec(string $command, mixed &$output = null, mixed &$resultCode = 0, bool $ignore = false): string|int|bool|null + { + $this->debugMode("exec( $command )"); + $lastLine = exec($command, $output, $resultCode); + if ($resultCode !== 0 and !$ignore) { + $this->consoleIO?->error($lastLine); + return $resultCode; + } + $this->debugMode("->> rc: $resultCode | last info: $lastLine"); + return $lastLine; + } + + /** + * @param string $command + * @param int|null $resultCode + * @param bool $ignore + * @return string|int|bool|null + */ + public function system(string $command, ?int &$resultCode = 0, bool $ignore = false): string|int|bool|null + { + $this->debugMode("system( $command )"); + $info = system($command, $resultCode); + if ($resultCode !== 0) { + if (!$ignore) { + $this->consoleIO?->error($info); + return $resultCode; + } + } + $this->debugMode("->> rc: $resultCode | last info: $info"); + return $info; + } + + /** + * @param string $command + * @param string|null $lastLine + * @return int resultCode + */ + public function execWithProgress(string $command, ?string &$lastLine = null): int + { + $this->debugMode("execWithProgress( $command )"); + $process = popen($command, 'r'); + while (!feof($process)) { + $line = fgets($process); + if ($line === false) { + break; + } else { + $lastLine = trim($line); + if ($lastLine) { + $this->consoleIO?->subOutput($lastLine); + } + } + usleep(1000); + } + $this->debugMode("> rc: null | last info: $lastLine"); + return pclose($process); + } +} diff --git a/tools/src/Phpy/Helpers/System.php b/tools/src/Phpy/Helpers/System.php new file mode 100644 index 0000000..3bffe90 --- /dev/null +++ b/tools/src/Phpy/Helpers/System.php @@ -0,0 +1,250 @@ + + */ + protected static array $fileContentCache = []; + + /** + * @var array + */ + protected static array $existingPackages = []; + + /** + * @return false|string + */ + public static function getcwd() + { + return $GLOBALS['PHPY_CWD'] ?? getcwd(); + } + + /** + * @param string $cwd + * @return void + */ + public static function setcwd(string $cwd) + { + $GLOBALS['PHPY_CWD'] = $cwd; + } + + /** + * 获取/设置 python所在路径 + * + * @param string|null $path + * @return string + */ + public static function python(?string $path = null): string + { + if (file_exists($command = System::getcwd() . '/python.command')) { + return System::getFileContent($command); + } + if (!$path or !file_exists($path)) { + if (!$path = exec('command -v python')) { + throw new PhpyException('Python not found. '); + } + } + System::putFileContent($command, $path); + return $path; + } + + /** + * 获取/设置 pip所在路径 + * + * @param string|null $path + * @return string + */ + public static function pip(?string $path = null): string + { + if (file_exists($command = System::getcwd() . '/pip.command')) { + return System::getFileContent($command); + } + if (!$path or !file_exists($path)) { + if (!$path = exec('command -v pip')) { + throw new PhpyException('Python-pip not found. '); + } + } + System::putFileContent($command, $path); + return $path; + } + + /** + * 获取/设置 python-config所在路径 + * + * @param string|null $path + * @return string + */ + public static function pythonConfig(?string $path = null): string + { + if (file_exists($command = System::getcwd() . '/python-config.command')) { + return System::getFileContent($command); + } + if (!$path or !file_exists($path)) { + if (!$path = exec('command -v python-config')) { + throw new PhpyException('Python-config not found. '); + } + } + System::putFileContent($command, $path); + return $path; + } + + /** + * 获取文件内容 + * + * @param string $filePath + * @param bool $cache + * @return string + */ + public static function getFileContent(string $filePath, bool $cache = true): string + { + if ($cache) { + return static::$fileContentCache[$filePath] ??= file_get_contents($filePath); + } + return file_get_contents($filePath); + } + + /** + * 写入文件内容 + * + * @param string $filePath + * @param string $content + * @param int $flag + * @param bool $cache + * @return bool|int + */ + public static function putFileContent(string $filePath, string $content, int $flag = 0, bool $cache = true): bool|int + { + if (!$cache or $flag !== 0) { + unset(static::$fileContentCache[$filePath]); + } else { + static::$fileContentCache[$filePath] = $content; + } + return file_put_contents($filePath, $content, $flag); + } + + /** + * 清除文件内容缓存 + * + * @return void + */ + public static function clearFileContentCache(): void + { + static::$fileContentCache = []; + } + + + /** + * 获取系统类型 + * + * @return string + */ + public static function getPackageManager(): string + { + return match (true) { + file_exists('/etc/alpine-release') => 'apk', + file_exists('/etc/centos-release') || + file_exists('/etc/redhat-release') || + file_exists('/etc/fedora-release') => 'yum', + file_exists('/etc/arch-release') => 'pacman', + file_exists('/etc/SuSE-release') => 'zypper', + stripos(php_uname('s'), 'Darwin') !== false => 'brew', + stripos(php_uname('s'), 'Windows') !== false => 'winget', + default => 'apt-get', + }; + } + + /** + * @return string[] + */ + public static function getBuildToolsList(): array + { + return match (static::getPackageManager()) { + 'yum', 'zypper' => ['gcc', 'gcc-c++', 'make', 'autoconf'], + 'apk', 'apt-get' => ['gcc', 'g++', 'make', 'autoconf'], + 'brew' => ['autoconf'], + 'pacman' => ['gcc', 'make', 'autoconf'], + 'winget' => ['make', 'autoconf'], + default => [], + }; + } + + /** + * 获取系统编译依赖卸载命令 + * + * @return string|null + */ + public static function getBuildToolsUninstall(): ?string + { + $uninstallPackages = array_diff(static::getBuildToolsList(), static::$existingPackages); + static::$existingPackages = []; + return match (static::getPackageManager()) { + 'apk' => 'apk del ' . implode(' ', $uninstallPackages), + 'yum' => 'sudo yum remove ' . implode(' ', $uninstallPackages), + 'brew' => 'brew uninstall ' . implode(' ', $uninstallPackages), + 'zypper' => 'sudo zypper remove ' . implode(' ', $uninstallPackages), + 'pacman' => 'sudo pacman -R ' . implode(' ', $uninstallPackages), + 'winget' => 'winget uninstall ' . implode(' ', $uninstallPackages), + 'apt-get' => 'sudo apt-get install ' . implode(' ', $uninstallPackages), + default => null, + }; + } + + /** + * 获取系统编译依赖安装命令 + * + * @return string|null + */ + public static function getBuildToolsInstall(): ?string + { + self::$existingPackages = self::checkExistingPackages($packages = static::getBuildToolsList()); + + $installPackages = array_diff($packages, self::$existingPackages); + + return match (self::getPackageManager()) { + 'apk' => 'apk add --no-cache ' . implode(' ', $installPackages), + 'yum' => 'sudo yum install ' . implode(' ', $installPackages), + 'brew' => 'brew install ' . implode(' ', $installPackages), + 'zypper' => 'sudo zypper install ' . implode(' ', $installPackages), + 'pacman' => 'sudo pacman -S ' . implode(' ', $installPackages), + 'winget' => 'winget install ' . implode(' ', $installPackages), + 'apt-get' => 'sudo apt-get install ' . implode(' ', $installPackages), + default => null, + }; + } + + /** + * 检查系统中已存在的包 + * + * @param array $packages + * @return array + */ + protected static function checkExistingPackages(array $packages): array + { + $existingPackages = []; + foreach ($packages as $package) { + $result = match (self::getPackageManager()) { + 'apk' => shell_exec("apk info | grep ^$package\$"), + 'yum' => shell_exec("rpm -q $package"), + 'brew' => shell_exec("brew list --formula | grep ^$package\$"), + 'zypper' => shell_exec("zypper se -i | grep ^$package\$"), + 'pacman' => shell_exec("pacman -Qi $package"), + 'winget' => shell_exec("winget list $package"), + default => shell_exec("dpkg -l | grep ^ii | grep $package") || + shell_exec("apt list --installed | grep ^$package/"), + }; + + if (!empty(trim($result))) { + $existingPackages[] = $package; + } + } + return $existingPackages; + } +} diff --git a/tools/src/Phpy/Installers/BuildToolsInstaller.php b/tools/src/Phpy/Installers/BuildToolsInstaller.php new file mode 100644 index 0000000..249c392 --- /dev/null +++ b/tools/src/Phpy/Installers/BuildToolsInstaller.php @@ -0,0 +1,83 @@ +config = $config; + $this->consoleIO = $consoleIO; + $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); + } + + /** @inheritdoc */ + public function install(): void + { + // 编译依赖工具 + $command = System::getBuildToolsInstall(); + if ($this->consoleIO?->ask( + "Do you want to install dependency tools? $command [Y,n]", + true, + questionClass: ConfirmationQuestion::class + )) { + if ($this->process->execWithProgress( + "$command" + ) !== 0) { + throw new CommandFailedException('Error installing dependency tools.'); + } + } + } + + /** @inheritdoc */ + public function uninstall(): void + { + // 卸载编译依赖工具 + $command = System::getBuildToolsUninstall(); + if ($this->consoleIO?->ask( + "Do you want to uninstall dependency tools?> [y,N])", + false, + questionClass: ConfirmationQuestion::class + )) { + $this->process->execWithProgress( + "$command" + ); + } + } + + /** @inheritdoc */ + public function upgrade(): void + { + $this->install(); + } + + /** @inheritdoc */ + public function clearCache(): void + { + } +} diff --git a/tools/src/Phpy/Installers/InstallerInterface.php b/tools/src/Phpy/Installers/InstallerInterface.php new file mode 100644 index 0000000..2e52c2e --- /dev/null +++ b/tools/src/Phpy/Installers/InstallerInterface.php @@ -0,0 +1,48 @@ +config = $config; + $this->consoleIO = $consoleIO; + $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); + if (!$config->get('module')) { + $this->skipInfo = 'Module not configured. Skip install.'; + return; + } + } + + /** + * 查询 pip 库中的模块版本 + * + * @param string $module + * @return array|null + */ + protected function moduleVersions(string $module): ?array + { + $res = $this->process->pipExec("index versions $module"); + if (!str_contains($res, 'ERROR')) { + // 解析 pip 输出,获取模块的可用版本 + preg_match_all('/Available versions: (.+)/', $res, $matches); + return explode(', ', $matches[1][0] ?? ''); + } + return null; + } + + /** @inheritdoc */ + public function install(): void + { + $modules = $this->config->get('modules', []); + $installModules = []; + foreach ($modules as $module => $versionConstraint) { + // 查询 pip 库中的模块版本 + if (!$availableVersions = $this->moduleVersions($module)) { + $this->consoleIO?->output(<<$module not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Install failed.'); + } + // 检查版本是否满足约束 + $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); + if (!$satisfyingVersions) { + $this->consoleIO?->output(<<$module version $versionConstraint> not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Install failed.'); + } + // 选择满足约束的版本 + $installModules[$module] = Semver::rsort($satisfyingVersions); + } + + // 没有hash,说明是json文件安装,则扫描vendor + if (!$this->config->get('hash')) { + // vendor + $vendorModules = []; + Application::getVendorConfigFiles(function ($organization, $package, $configFilePath) use (&$vendorModules) { + $config = new Config($configFilePath); + $modules = $config->get('modules', []); + foreach ($modules as $module => $versionConstraint) { + $vendorModules[$module][$versionConstraint] = [ + 'organization' => $organization, + 'package' => $package, + ]; + } + }); + foreach ($vendorModules as $module => $item) { + // others + if (!isset($installModules[$module])) { + // 查询 pip 库中的模块版本 + if (!$availableVersions = $this->moduleVersions($module)) { + $this->consoleIO?->output(<<$module not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Install failed.'); + } + } + // exits + else { + $availableVersions = $installModules[$module]; + } + foreach ($item as $versionConstraint => $info) { + // 检查版本是否满足约束 + $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); + if (!$satisfyingVersions) { + $this->consoleIO?->output(<<{$info['organization']}/{$info['package']} -- +Python module $module version-constraint $versionConstraint> not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Install failed.'); + } + $availableVersions = Semver::rsort($satisfyingVersions); + } + $installModules[$module] = $availableVersions; + } + } + if ($installModules) { + // 生成 requirements.txt 且安装 + $installModulesContent = ''; + foreach ($installModules as $module => $versions) { + $this->config->set("modules.$module", $version = $versions[0]); + $installModulesContent .= "$module==$version\n"; + } + System::putFileContent($requirementsFile = System::getcwd() . '/requirements.txt', $installModulesContent, cache: false); + $this->consoleIO->subOutput(<<config->get('config.pip-index-url')) { + $this->process->pipExec("config set global.index-url $pipGlobalIndex"); + } + if ($this->process->pipExec("install -r $requirementsFile") !== 0) { + throw new CommandFailedException('Install failed.'); + } + } else { + $this->consoleIO->output('No modules to install.'); + } + } + + /** @inheritdoc */ + public function uninstall(): void + { + } + + /** @inheritdoc */ + public function upgrade(): void + { + $modules = $this->config->get('modules', []); + $this->config->set('local-modules', $modules); + $installModules = []; + foreach ($modules as $module => $versionConstraint) { + // 查询 pip 库中的模块版本 + if (!$availableVersions = $this->moduleVersions($module)) { + $this->consoleIO?->output(<<$module not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Update failed.'); + } + // 检查版本是否满足约束 + $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); + if (!$satisfyingVersions) { + $this->consoleIO?->output(<<$module version-constraint $versionConstraint> not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Update failed.'); + } + // 选择满足约束的版本 + $installModules[$module] = Semver::rsort($satisfyingVersions); + } + // vendor + $vendorModules = []; + Application::getVendorConfigFiles(function ($organization, $package, $configFilePath) use (&$vendorModules) { + $config = new Config($configFilePath); + $modules = $config->get('modules', []); + foreach ($modules as $module => $versionConstraint) { + $vendorModules[$module][$versionConstraint] = [ + 'organization' => $organization, + 'package' => $package, + ]; + } + }); + foreach ($vendorModules as $module => $item) { + // others + if (!isset($installModules[$module])) { + // 查询 pip 库中的模块版本 + if (!$availableVersions = $this->moduleVersions($module)) { + $this->consoleIO?->output(<<$module not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Update failed.'); + } + } + // exits + else { + $availableVersions = $installModules[$module]; + } + foreach ($item as $versionConstraint => $info) { + // 检查版本是否满足约束 + $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); + if (!$satisfyingVersions) { + $this->consoleIO?->output(<<{$info['organization']}/{$info['package']} -- +Python module $module version-constraint $versionConstraint> not found in pip. + +Read more about https://pypi.org/search +EOT + ); + throw new CommandFailedException('Update failed.'); + } + $availableVersions = Semver::rsort($satisfyingVersions); + } + $installModules[$module] = $availableVersions; + } + if ($installModules) { + // 生成 requirements.txt 且安装 + $installModulesContent = ''; + foreach ($installModules as $module => $versions) { + $this->config->set("modules.$module", $version = $versions[0]); + $installModulesContent .= "$module==$version\n"; + } + System::putFileContent($requirementsFile = System::getcwd() . '/requirements.txt', $installModulesContent, cache: false); + $this->consoleIO->subOutput(<<config->get('config.pip-index-url')) { + $this->process->pipExec("config set global.index-url $pipGlobalIndex"); + } + if ($this->process->pipExec("install -r $requirementsFile") !== 0) { + throw new CommandFailedException('Update failed.'); + } + } + $this->config->set('vendor-modules', $vendorModules); + } + + /** @inheritdoc */ + public function clearCache(): void + { + } +} diff --git a/tools/src/Phpy/Installers/PhpyInstaller.php b/tools/src/Phpy/Installers/PhpyInstaller.php new file mode 100644 index 0000000..be042ac --- /dev/null +++ b/tools/src/Phpy/Installers/PhpyInstaller.php @@ -0,0 +1,115 @@ +config = $config; + $this->consoleIO = $consoleIO; + $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); + if (!$config->get('phpy')) { + $this->skipInfo = 'PHPy not configured. Skip install.'; + return; + } + if (extension_loaded('phpy')){ + $this->skipInfo = "PHPy already installed."; + return; + } + } + + /** @inheritdoc */ + public function install(): void + { + if ($this->skipInfo) { + $this->consoleIO?->output($this->skipInfo); + return; + } + $url = $this->config->get('phpy.source-url'); + $version = $this->config->get('phpy.version', 'latest'); + $cacheDir = $this->config->get('config.cache-dir'); + $versionOpt = ($version === 'latest') ? '' : "--branch $version"; + // 下载源码 + $sourceDir = "$cacheDir/phpy-$version"; + if (!file_exists($sourceDir)) { + $this->consoleIO?->output('PHPy-source Downloading ...'); + if ($this->process->execWithProgress( + "git clone --depth 1 $versionOpt $url $sourceDir" + ) !== 0) { + throw new CommandFailedException('Error downloading PHPy-source.'); + } + $this->consoleIO?->output('PHPy-source Downloaded.'); + } + // 编译安装 + $pythonConfigDir = System::pythonConfig(); + $phpyInstallConfigure = $this->config->get('phpy.install-configure') ?: [ + "--with-python-config=$pythonConfigDir" + ]; + $phpyInstallConfigure = implode(' ', $phpyInstallConfigure); + $phpIniPath = $this->config->get('phpy.ini-path'); + $iniCmd = $phpIniPath ? "&& echo 'extension=phpy.so' > $phpIniPath" : ''; + if ( + $this->process->execWithProgress( + "cd $sourceDir && phpize && ./configure $phpyInstallConfigure && make clean && make && make install $iniCmd" + ) !== 0 + ) { + throw new CommandFailedException('Error building and installing PHPy extension.'); + } + } + + /** @inheritdoc */ + public function uninstall(): void + { + $phpIniPath = $this->config->get('phpy.ini-path'); + if (file_exists($phpIniPath)) { + $this->process->exec("rm $phpIniPath"); + } + } + + /** @inheritdoc */ + public function upgrade(): void + { + $this->install(); + } + + /** @inheritdoc */ + public function clearCache(): void + { + $cacheDir = $this->config->get('config.cache-dir'); + if ($this->process->execWithProgress( + "rm -rf $cacheDir/phpy-*" + ) !== 0) { + throw new CommandFailedException('Error clearing PHPy cache.'); + } + } +} diff --git a/tools/src/Phpy/Installers/PythonInstaller.php b/tools/src/Phpy/Installers/PythonInstaller.php new file mode 100644 index 0000000..fcbf73d --- /dev/null +++ b/tools/src/Phpy/Installers/PythonInstaller.php @@ -0,0 +1,204 @@ +config = $config; + $this->consoleIO = $consoleIO; + $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); + if (!$config->get('python')) { + $this->skipInfo = 'Python not configured. Skip install.'; + return; + } + if ($pythonInstallPath = $config->get('python.install-path', '/usr/bin/python')){ + if (file_exists($pythonInstallPath)) { + $this->skipInfo = "Python already installed at $pythonInstallPath."; + return; + } + } + } + + /** @inheritdoc */ + public function install(): void + { + $version = $this->config->get('python.version', 'latest'); + $cacheDir = $this->config->get('config.cache-dir'); + $installDir = $this->config->get('python.install-dir'); + + if (!$this->skipInfo) { + $url = $this->config->get('python.source-url'); + $versionOpt = ($version === 'latest') ? '' : "--branch $version"; + // 下载源码 + $sourceDir = "$cacheDir/python-$version"; + if (!file_exists($sourceDir)) { + $this->consoleIO?->output('CPython-source Downloading ...'); + if ($this->process->execWithProgress( + "git clone --depth 1 $versionOpt $url $sourceDir" + ) !== 0) { + throw new CommandFailedException('Error downloading Python.'); + } + $this->consoleIO?->output('CPython-source Downloaded.'); + } + + // 编译安装 + $pythonInstallConfigure = $this->config->get('python.install-configure', []); + $pythonInstallConfigure[] = "--prefix=$installDir"; + $pythonInstallConfigure = implode(' ', $pythonInstallConfigure); + $this->consoleIO?->output("Building and installing Python-$version..."); + if ( + $this->process->execWithProgress( + "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install" + ) !== 0 + ) { + throw new CommandFailedException("Error building and installing Python-$version."); + } + } + + $python = $installDir . '/bin/python'; + $pip = $installDir . '/bin/pip'; + $cwd = System::getcwd(); + // 虚拟环境 + if (!file_exists($venvPath = "$cwd/py-vendor/.venv")) { + // 安装虚拟 + $this->process->execWithProgress("$python -m venv $venvPath"); + $this->process->execWithProgress("source $venvPath/bin/activate"); + // 软链python-config + $pythonConfigPath = "$venvPath/bin/python-config"; + $this->process->execWithProgress("ln -s $installDir/bin/python-config $pythonConfigPath"); + // 软链python-include + $this->process->execWithProgress("rm -rf $venvPath/include/python"); + $this->process->execWithProgress("ln -s $installDir/include/python $venvPath/include"); + // 设置环境 + $this->process->execWithProgress("echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command"); + } + } + + /** @inheritdoc */ + public function uninstall(): void + { + $version = $this->config->get('python.version', 'latest'); + $cacheDir = $this->config->get('config.cache-dir'); + // 卸载源码 + $sourceDir = "$cacheDir/python-$version"; + if (file_exists($sourceDir)) { + $this->process->exec("rm -rf $sourceDir"); + } + // 卸载虚拟环境 + $cwd = System::getcwd(); + if (file_exists($venvPath = "$cwd/py-vendor/.venv")) { + $this->process->exec("rm -rf $venvPath"); + } + } + + /** @inheritdoc */ + public function upgrade(): void + { + $version = $this->config->get('python.version', 'latest'); + $cacheDir = $this->config->get('config.cache-dir'); + $installDir = $this->config->get('python.install-dir'); + + if (!$this->skipInfo) { + $url = $this->config->get('python.source-url'); + $versionOpt = ($version === 'latest') ? '' : "--branch $version"; + // 下载源码 + $sourceDir = "$cacheDir/python-$version"; + if (file_exists($sourceDir)) { + $this->process->exec("rm -rf $sourceDir"); + } + $this->consoleIO?->output('CPython-source Downloading ...'); + if ($this->process->execWithProgress( + "git clone --depth 1 $versionOpt $url $sourceDir" + ) !== 0) { + throw new CommandFailedException('Error downloading Python.'); + } + $this->consoleIO?->output('CPython-source Downloaded.'); + + // 编译安装 + $pythonInstallConfigure = $this->config->get('python.install-configure', []); + $pythonInstallConfigure[] = "--prefix=$installDir"; + $pythonInstallConfigure = implode(' ', $pythonInstallConfigure); + $this->consoleIO?->output("Building and installing Python-$version..."); + if ( + $this->process->execWithProgress( + "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install" + ) !== 0 + ) { + throw new CommandFailedException("Error building and installing Python-$version."); + } + } + + $python = $installDir . '/bin/python'; + $pip = $installDir . '/bin/pip'; + $cwd = System::getcwd(); + // 虚拟环境 + if (file_exists($venvPath = "$cwd/py-vendor/.venv")) { + $this->process->exec("rm -rf $venvPath"); + } + // 安装虚拟 + $this->process->execWithProgress("$python -m venv $venvPath"); + $this->process->execWithProgress("source $venvPath/bin/activate"); + // 软链python-config + $pythonConfigPath = "$venvPath/bin/python-config"; + $this->process->execWithProgress("ln -s $installDir/bin/python-config $pythonConfigPath"); + // 软链python-include + $this->process->execWithProgress("rm -rf $venvPath/include/python"); + $this->process->execWithProgress("ln -s $installDir/include/python $venvPath/include"); + // 设置环境 + $this->process->execWithProgress("echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command"); + } + + /** @inheritdoc */ + public function clearCache(): void + { + $cacheDir = $this->config->get('config.cache-dir'); + if ($this->process->execWithProgress( + "rm -rf $cacheDir/python-*" + ) !== 0) { + throw new CommandFailedException('Error clearing Python cache.'); + } + } +} From 7cab686fe60c82d4261c877916cd7167ee048550 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 18 Mar 2025 12:56:18 +0800 Subject: [PATCH 2/6] feature phpy --- .gitignore | 1 + phpy.json | 25 ++ tools/src/PackageCollector.php | 56 --- tools/src/Phpy/Application.php | 21 +- tools/src/Phpy/Commands/InitConfigCommand.php | 47 +++ tools/src/Phpy/Commands/InstallCommand.php | 5 +- tools/src/Phpy/Commands/PhpyInstall.php | 160 +++----- tools/src/Phpy/Commands/PipMirrorConfig.php | 22 +- tools/src/Phpy/Commands/PipModuleInstall.php | 50 +-- tools/src/Phpy/Commands/PythonInstall.php | 106 ++--- tools/src/Phpy/Commands/ScanCommand.php | 78 ++++ tools/src/Phpy/Commands/ScanImport.php | 97 ++--- tools/src/Phpy/Commands/ShowCommand.php | 12 +- tools/src/Phpy/Commands/UpdateCommand.php | 10 +- tools/src/Phpy/Config.php | 28 +- tools/src/Phpy/ConsoleIO.php | 20 +- tools/src/Phpy/Helpers/PackageCollector.php | 70 ++++ tools/src/Phpy/Helpers/Process.php | 122 +++--- .../src/{ => Phpy/Helpers}/PythonMetadata.php | 2 +- tools/src/Phpy/Helpers/System.php | 6 +- .../Phpy/Installers/BuildToolsInstaller.php | 8 +- tools/src/Phpy/Installers/ModuleInstaller.php | 382 ++++++++++-------- tools/src/Phpy/Installers/PhpyInstaller.php | 15 +- tools/src/Phpy/Installers/PythonInstaller.php | 62 +-- 24 files changed, 759 insertions(+), 646 deletions(-) create mode 100644 phpy.json delete mode 100644 tools/src/PackageCollector.php create mode 100644 tools/src/Phpy/Commands/InitConfigCommand.php create mode 100644 tools/src/Phpy/Commands/ScanCommand.php create mode 100644 tools/src/Phpy/Helpers/PackageCollector.php rename tools/src/{ => Phpy/Helpers}/PythonMetadata.php (98%) diff --git a/.gitignore b/.gitignore index c7e107a..e222716 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,4 @@ config.sub /*.command /py-vendor /requirements.txt +/phpy.lock diff --git a/phpy.json b/phpy.json new file mode 100644 index 0000000..94e0b7d --- /dev/null +++ b/phpy.json @@ -0,0 +1,25 @@ +{ + "config": { + "cache-dir": "~/.cache/phpy", + "scan-dirs": [ + ], + "pip-index-url": "" + }, + "python": { + "source-url": "https://github.com/python/cpython.git", + "install-dir": "/usr", + "install-version": "latest", + "install-configure": [ + "--enable-optimizations", + "--with-lto", + "--enable-static" + ] + }, + "phpy": { + "source-url": "https://github.com/swoole/phpy.git", + "install-version": "latest", + "install-configure": [], + "ini-path": "/usr/local/etc/php/conf.d/xx-php-ext-phpy.ini" + }, + "modules": {} +} \ No newline at end of file diff --git a/tools/src/PackageCollector.php b/tools/src/PackageCollector.php deleted file mode 100644 index 1c65a13..0000000 --- a/tools/src/PackageCollector.php +++ /dev/null @@ -1,56 +0,0 @@ -args[0]) && $node->args[0]->value instanceof Node\Scalar\String_) { - $this->packages[] = $node->args[0]->value->value; - } - }; - if ($node instanceof Node\Expr\StaticCall) { - if ($node->class instanceof Node\Name && strval($node->class) === 'PyCore' && strval($node->name) === 'import') { - $foundPackageFn($node); - } - } elseif ($node instanceof Node\Expr\FuncCall) { - if ($node->name instanceof Node\Name && strtolower($node->name) === 'pyimport') { - $foundPackageFn($node); - } - } - } - - public function getPackages(): array - { - return array_unique($this->packages); // 去重 - } - - static function parseFile($filePath): array - { - $code = file_get_contents($filePath); - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $traverser = new NodeTraverser; - - $collector = new PackageCollector(); - $traverser->addVisitor($collector); - - try { - $statements = $parser->parse($code); - $traverser->traverse($statements); - } catch (Error $e) { - echo 'Parse Error: ' . $e->getMessage(); - } - - return $collector->getPackages(); - } -} diff --git a/tools/src/Phpy/Application.php b/tools/src/Phpy/Application.php index eba885e..12ca8bb 100644 --- a/tools/src/Phpy/Application.php +++ b/tools/src/Phpy/Application.php @@ -5,11 +5,13 @@ namespace PhpyTool\Phpy; use Closure; +use PhpyTool\Phpy\Commands\InitConfigCommand; use PhpyTool\Phpy\Commands\InstallCommand; use PhpyTool\Phpy\Commands\PhpyInstall; use PhpyTool\Phpy\Commands\PipMirrorConfig; use PhpyTool\Phpy\Commands\PipModuleInstall; use PhpyTool\Phpy\Commands\PythonInstall; +use PhpyTool\Phpy\Commands\ScanCommand; use PhpyTool\Phpy\Commands\ShowCommand; use PhpyTool\Phpy\Commands\UpdateCommand; use PhpyTool\Phpy\Helpers\System; @@ -33,6 +35,8 @@ public function __construct() System::setcwd(getcwd()); parent::__construct('PHPy', static::VERSION); $this->addCommands([ + new ScanCommand(), + new InitConfigCommand(), new InstallCommand(), new UpdateCommand(), new ShowCommand(), @@ -137,9 +141,12 @@ public static function getRequirementsFile(string $dir): ?string public static function findConfigFile(?Closure $closure = null): string|null { $currentDir = $startDir = System::getcwd(); - while ($currentDir !== dirname($currentDir)) {dump(1); + while ($currentDir !== dirname($currentDir)) { if ($filePath = static::getConfigFile($currentDir)) { - return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + if ($closure) { + call_user_func($closure, $filePath, $currentDir, $startDir); + } + return $filePath; } $currentDir = dirname($currentDir); } @@ -157,7 +164,10 @@ public static function findLockFile(?Closure $closure = null): ?string $currentDir = $startDir = System::getcwd(); while ($currentDir !== dirname($currentDir)) { if ($filePath = static::getLockFile($currentDir)) { - return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + if ($closure) { + call_user_func($closure, $filePath, $currentDir, $startDir); + } + return $filePath; } $currentDir = dirname($currentDir); } @@ -175,7 +185,10 @@ public static function findRequirementsFile(?Closure $closure = null): ?string $currentDir = $startDir = System::getcwd(); while ($currentDir !== dirname($currentDir)) { if ($filePath = static::getRequirementsFile($currentDir)) { - return $closure ? call_user_func($closure, $filePath, $currentDir, $startDir) : $filePath; + if ($closure) { + call_user_func($closure, $filePath, $currentDir, $startDir); + } + return $filePath; } $currentDir = dirname($currentDir); } diff --git a/tools/src/Phpy/Commands/InitConfigCommand.php b/tools/src/Phpy/Commands/InitConfigCommand.php new file mode 100644 index 0000000..1a813d9 --- /dev/null +++ b/tools/src/Phpy/Commands/InitConfigCommand.php @@ -0,0 +1,47 @@ +setName('init-config') + ->setDescription('Initialize the configuration file.') + ->setHelp( + <<init-config command initializes the configuration file. +If the configuration file already exists, it will not be overwritten. +If the configuration file does not exist, it will be created. + +PHPy introduces modules through Python-pip, read more at +https://pypi.org/help/ +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + if (!$jsonFile = Application::findConfigFile(function ($file, $cDir, $sDir) { + System::setcwd($cDir); + })) { + $config = new Config(); + System::putFileContent($jsonFile = System::getcwd() . '/phpy.json', (string)$config, cache: false); + } + return $this->consoleIO?->success($jsonFile + ? "The configuration file already exists [$jsonFile]." + : "The configuration file has been created [$jsonFile]." + ); + } +} diff --git a/tools/src/Phpy/Commands/InstallCommand.php b/tools/src/Phpy/Commands/InstallCommand.php index 22aa930..153b058 100644 --- a/tools/src/Phpy/Commands/InstallCommand.php +++ b/tools/src/Phpy/Commands/InstallCommand.php @@ -77,6 +77,8 @@ protected function handler(): int $this->consoleIO?->comment("No phpy.lock file present. Updating dependencies to latest instead of installing from lock file."); } $config = new Config($lockFile ?: $jsonFile); + // build tools + (new BuildToolsInstaller($config, $this->consoleIO))->install(); // install python env if (!$this->consoleIO?->getInput()->getOption('skip-env')) { (new PythonInstaller($config, $this->consoleIO))->install(); @@ -87,13 +89,10 @@ protected function handler(): int } // install modules if (!$this->consoleIO?->getInput()->getOption('skip-module')) { - // build tools - (new BuildToolsInstaller($config, $this->consoleIO))->install(); // module (new ModuleInstaller($config, $this->consoleIO))->install(); } if (!$lockFile) { - $config->set('hash', hash_file('SHA-256', $jsonFile)); Application::setLockFile(System::getcwd(), $config->all()); } } catch (CommandStopException) { diff --git a/tools/src/Phpy/Commands/PhpyInstall.php b/tools/src/Phpy/Commands/PhpyInstall.php index f2f323f..43e00c1 100644 --- a/tools/src/Phpy/Commands/PhpyInstall.php +++ b/tools/src/Phpy/Commands/PhpyInstall.php @@ -4,10 +4,15 @@ namespace PhpyTool\Phpy\Commands; +use PhpyTool\Phpy\Config; +use PhpyTool\Phpy\Exceptions\CommandFailedException; +use PhpyTool\Phpy\Exceptions\CommandStopException; +use PhpyTool\Phpy\Exceptions\CommandSuccessedException; +use PhpyTool\Phpy\Installers\BuildToolsInstaller; +use PhpyTool\Phpy\Installers\PhpyInstaller; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Question\ConfirmationQuestion; -use Symfony\Component\Console\Question\Question; class PhpyInstall extends AbstractCommand { @@ -24,122 +29,51 @@ protected function configure(): void /** @inheritdoc */ protected function handler(): int { - $helper = new QuestionHelper(); - $version = $this->consoleIO?->getInput()->getArgument('version'); - // 询问安装目录 - $installDir = $this->consoleIO - ?->ask('Please specify the installation directory (default: .runtime):', getcwd() . '/.runtime') - . '/swoole_phpy_' . str_replace('.', '', $version); - - if (!file_exists($installDir)) { - // 下载源码 - $this->consoleIO?->output('Downloading the latest source code ...'); - if ( - $this->execWithProgress($version === 'latest' ? - "git clone --depth 1 https://github.com/swoole/phpy.git $installDir" : - "git clone --depth 1 --branch $version https://github.com/swoole/phpy.git $installDir") !== 0 - ) { - return $this->consoleIO?->error('Error downloading source code.'); - } + $config = new Config(); + $pythonSourceUrl = $config->get('phpy.source-url', 'https://github.com/swoole/phpy.git'); + $config->set('phpy.source-url', $this->consoleIO?->ask( + "Please enter the ext-phpy source code URL to install (default: $pythonSourceUrl).", + $pythonSourceUrl + )); + + $pythonVersion = $config->get('phpy.install-version', 'latest'); + $config->set('phpy.install-version', $this->consoleIO?->ask( + "Please enter the ext-phpy version to install (default: $pythonVersion).", + $pythonVersion + )); + + if ($this->consoleIO?->ask( + 'Do you want to add ext-phpy in php.ini? [Y,n]:', + false, + questionClass: ConfirmationQuestion::class + )) { + $phpyIniPath = $config->get('phpy.ini-path', '/usr/local/etc/php/conf.d/xx-php-ext-phpy.ini'); + $config->set('phpy.ini-path', $this->consoleIO?->ask( + "Please enter the ext-phpy php.ini path (default: $phpyIniPath).", + $phpyIniPath + )); } else { - $this->consoleIO?->comment('PHPy source code already downloaded.'); - } - - // 安装编译依赖组件 - $this->consoleIO?->output('Installing dependencies...'); - if ($installCommands = $this->getSystemInstallCommands()) { - if ($this->execWithProgress($installCommands) !== 0) { - return $this->consoleIO?->error('Error installing dependencies.'); - } - } else { - return $this->consoleIO?->error('Please install PHPy manually.'); - } - - $question = new Question("[?] Please specify the Python-config directory (default: /usr/bin/python-config): \n", '/usr/bin/python-config'); - $pythonConfigDir = $helper->ask($this->getInput(), $this->getOutput(), $question); - // 编译并安装拓展 - $this->output('Building and installing PHPy extension...'); - if ( - $this->execWithProgress( - "cd $installDir && phpize && ./configure --with-python-config=$pythonConfigDir && make clean && make && make install" - ) !== 0 - ) { - return $this->error('Error building and installing PHPy extension.'); + $config->set('phpy.ini-path', null); } - - // 询问是否移除源码 - $question = new ConfirmationQuestion("[?] Do you want to add ext-PHPy in php.ini? [Y/n]: \n", true); - if ($helper->ask($this->getInput(), $this->getOutput(), $question)) { - $question = new Question("[?] Please specify the php.ini path (default: /usr/local/etc/php/conf.d/xx-php-ext-phpy.ini): \n", '/usr/local/etc/php/conf.d/xx-php-ext-phpy.ini'); - $phpIniPath = $helper->ask($this->getInput(), $this->getOutput(), $question); - $this->system('echo "extension=phpy.so" > ' . $phpIniPath, $rc, true); - if ($rc !== 0) { - $this->error('Error removing source code. Your can remove it manually.'); + try { + $buildTools = new BuildToolsInstaller($config, $this->consoleIO); + $buildTools->install(); + (new PhpyInstaller($config, $this->consoleIO))->install(); + if ($this->consoleIO?->ask( + 'Do you need to uninstall the build dependencies? [n,Y]', + false, + questionClass: ConfirmationQuestion::class + )) { + $buildTools->uninstall(); } + } catch (CommandStopException) { + return $this->consoleIO?->success('Installation stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); } - // 询问是否移除源码 - $question = new ConfirmationQuestion("[?] Do you want to remove the source code? (path: $installDir) [Y/n]: \n", true); - if ($helper->ask($this->getInput(), $this->getOutput(), $question)) { - $this->system('rm -rf ' . $installDir, $rc, true); - if ($rc !== 0) { - $this->error('Error removing source code. Your can remove it manually.'); - } - } - - // 询问是否卸载编译依赖组件 - $question = new ConfirmationQuestion("[?] Do you want to remove the build dependencies? [Y/n]: \n", true); - if ($helper->ask($this->getInput(), $this->getOutput(), $question)) { - $removeCommands = $this->getSystemUninstallCommands(); - if ($removeCommands) { - if ($this->execWithProgress($removeCommands) !== 0) { - $this->error('Error removing build dependencies. Your can remove it manually.'); - } - } - } - - // 询问是composer安装swoole/phpy - $question = new ConfirmationQuestion("[?] Do you want to require swoole/phpy? [y/N]: \n", false); - if ($helper->ask($this->getInput(), $this->getOutput(), $question)) { - if ($this->execWithProgress('composer require swoole/phpy --ignore-platform-req=ext-phpy') !== 0) { - $this->error('Error requiring PHPy via Composer. Your can require it manually.'); - } - } - - return $this->success('PHPy installation completed successfully. '); - } - - private function getSystemInstallCommands(): ?string - { - return match ($this->getSystemType()) { - 'alpine' => 'apk add --no-cache gcc g++ make autoconf', - 'redhat' => 'sudo yum install -y gcc gcc-c++ make autoconf', - 'darwin' => 'brew install autoconf', - 'windows' => null, - default => 'sudo apt-get install -y build-essential autoconf', - }; - } - - private function getSystemUninstallCommands(): ?string - { - return match ($this->getSystemType()) { - 'alpine' => 'apk del gcc g++ make autoconf', - 'redhat' => 'sudo yum remove -y gcc gcc-c++ make autoconf', - 'darwin' => 'brew uninstall autoconf', - 'windows' => null, - default => 'sudo apt-get remove -y build-essential autoconf', - }; - } - - private function getSystemType(): string - { - return match (true) { - file_exists('/etc/alpine-release') => 'alpine', - file_exists('/etc/centos-release') || - file_exists('/etc/redhat-release') => 'redhat', - stripos(php_uname('s'), 'Darwin') !== false => 'darwin', - stripos(php_uname('s'), 'Windows') !== false => 'windows', - default => 'linux', - }; + return $this->consoleIO?->success('PHPy installation completed successfully. '); } } diff --git a/tools/src/Phpy/Commands/PipMirrorConfig.php b/tools/src/Phpy/Commands/PipMirrorConfig.php index 3a4bea6..4caecaf 100644 --- a/tools/src/Phpy/Commands/PipMirrorConfig.php +++ b/tools/src/Phpy/Commands/PipMirrorConfig.php @@ -21,8 +21,8 @@ protected function configure(): void /** @inheritdoc */ protected function handler(): int { - $this->output('Current pip mirror url: ' . $this->exec('pip config get global.index-url')); - $this->output('Select the pip mirror source ...'); + $this->consoleIO?->output('Current pip mirror url: ' . $this->process->executePip('config get global.index-url')); + $this->consoleIO?->output('Select the pip mirror source ...'); $options = ['Python 官方', '清华', '阿里云', '华为云', '腾讯云', '中科大', '网易']; // 创建选择问题 @@ -35,18 +35,18 @@ protected function handler(): int $question->setErrorMessage('Invalid option.'); $helper = new QuestionHelper(); - $selectedOption = $helper->ask($this->input, $this->output, $question); + $selectedOption = $helper->ask($this->consoleIO?->getInput(), $this->consoleIO?->getOutput(), $question); match ($selectedOption) { - 'Python 官方' => $this->exec('pip config set global.index-url https://pypi.org/simple/'), - '清华' => $this->exec('pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple'), - '阿里云' => $this->exec('pip config set global.index-url http://mirrors.aliyun.com/pypi/simple/'), - '华为云' => $this->exec('pip config set global.index-url https://mirrors.huaweicloud.com/repository/pypi/simple/'), - '腾讯云' => $this->exec('pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple/'), - '中科大' => $this->exec('pip config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple/'), - '网易' => $this->exec('pip config set global.index-url http://mirrors.163.com/pypi/simple/'), + 'Python 官方' => $this->process->executePip('config set global.index-url https://pypi.org/simple/'), + '清华' => $this->process->executePip('config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple'), + '阿里云' => $this->process->executePip('config set global.index-url http://mirrors.aliyun.com/pypi/simple/'), + '华为云' => $this->process->executePip('config set global.index-url https://mirrors.huaweicloud.com/repository/pypi/simple/'), + '腾讯云' => $this->process->executePip('config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple/'), + '中科大' => $this->process->executePip('config set global.index-url https://pypi.mirrors.ustc.edu.cn/simple/'), + '网易' => $this->process->executePip('config set global.index-url http://mirrors.163.com/pypi/simple/'), }; - return $this->success('已将 pip 源设置为: ' . $selectedOption); + return $this->consoleIO?->success('已将 pip 源设置为: ' . $selectedOption); } } diff --git a/tools/src/Phpy/Commands/PipModuleInstall.php b/tools/src/Phpy/Commands/PipModuleInstall.php index 7d3a3e7..08d90e9 100644 --- a/tools/src/Phpy/Commands/PipModuleInstall.php +++ b/tools/src/Phpy/Commands/PipModuleInstall.php @@ -4,6 +4,8 @@ namespace PhpyTool\Phpy\Commands; +use PhpyTool\Phpy\Config; +use PhpyTool\Phpy\Installers\ModuleInstaller; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Question\Question; @@ -26,41 +28,27 @@ protected function configure(): void /** @inheritdoc */ protected function handler(): int { - $helper = new QuestionHelper(); - $module = $this->getInput()?->getArgument('module'); - $version = $this->getInput()?->getArgument('version'); + $module = $this->consoleIO?->getInput()->getArgument('module'); + $version = $this->consoleIO?->getInput()?->getArgument('version'); - $question = new Question("[?] Please specify the path of Python (default: /usr/bin/python3): \n", '/usr/bin/python3'); - $pythonPath = $helper->ask($this->getInput(), $this->getOutput(), $question); - if ( - !file_exists($pythonPath) or - version_compare( - $this->pythonVersion = substr($this->exec("$pythonPath --version", ignore: true), 7, 4), - '3.10', - '<' - ) - ) { - return $this->error('Please install Python 3.10+ manually.'); + $moduleInstaller = new ModuleInstaller(new Config(), $this->consoleIO); + $versions = $moduleInstaller->availableVersions($module); + if ($version === 'latest') { + $ver = $versions[0]; } - - $question = new Question("[?] Please specify the path of pip (default: /usr/bin/pip3): \n", '/usr/bin/pip3'); - $pipPath = $helper->ask($this->getInput(), $this->getOutput(), $question); - if (!file_exists($pipPath)) { - return $this->error('Please install Python-pip manually.'); + if ($index = array_search($version, $versions, true)) { + $ver = $versions[$index]; } - - $this->output("Installing Python module $module-$version ..."); - $moduleInstalled = $this->exec("$pipPath show $module", ignore: true); - if (!$moduleInstalled or ($version !== 'latest' and !str_contains($moduleInstalled, "Version: $version"))) { - if ($this->execWithProgress( - "$pipPath install $module" . ($version === 'latest' ? '' : "==$version") . ' --break-system-packages' - )) { - return $this->error("Error installing Python module $module-$version."); - } - } else { - $this->comment("Python module $module-$version is already installed."); + if ($ver ?? null) { + return $this->consoleIO?->error("Python module $module-$version is not available."); + } + if ( + $this->process->executePip("install $module==$version --break-system-packages", subOutput: true) + !== 0 + ) { + return $this->consoleIO?->error("Error installing Python module $module-$version."); } - return $this->success("Python module $module-$version installation complete."); + return $this->consoleIO?->success("Python module $module-$version installation complete."); } } diff --git a/tools/src/Phpy/Commands/PythonInstall.php b/tools/src/Phpy/Commands/PythonInstall.php index 6edb644..5859085 100644 --- a/tools/src/Phpy/Commands/PythonInstall.php +++ b/tools/src/Phpy/Commands/PythonInstall.php @@ -4,13 +4,17 @@ namespace PhpyTool\Phpy\Commands; +use PhpyTool\Phpy\Config; +use PhpyTool\Phpy\Exceptions\CommandFailedException; +use PhpyTool\Phpy\Exceptions\CommandStopException; +use PhpyTool\Phpy\Exceptions\CommandSuccessedException; +use PhpyTool\Phpy\Installers\BuildToolsInstaller; +use PhpyTool\Phpy\Installers\PythonInstaller; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Question\Question; class PythonInstall extends AbstractCommand { - protected string $pythonVersion = '3.10'; /** @inheritdoc */ protected function configure(): void @@ -25,71 +29,43 @@ protected function configure(): void /** @inheritdoc */ protected function handler(): int { - $this->output('Checking and installing Python-3.10+ Python-pip Python-dev ...'); - if (!$installCommands = $this->getPythonInstallCommands()) { - return $this->error('Unsupported operating system.'); - } - $this->execWithProgress($installCommands, $lastLine); - if (!str_starts_with($lastLine, 'OK:')) { - return $this->error('Failed to install Python-3.10+ Python-pip Python-dev.'); - } - // 再次检查Python版本 - if (version_compare( - $this->pythonVersion = substr($this->exec('python3 --version', ignore: true), 7, 4), - '3.10', - '<' - ) - ) { - return $this->error('Python version must be 3.10 or higher.'); - } - - // 安装虚拟环境 - if ($this->getInput()?->getOption('venv')) { - $helper = new QuestionHelper(); - $question = new Question("[?] Please specify the path of Python-venv (default: getcwd()/.venv): \n", null); - $venvPath = $helper->ask($this->getInput(), $this->getOutput(), $question); - // 检查虚拟环境是否已经存在 - $venvPath = $venvPath ?: getcwd(). '/.venv'; - $this->output("Setting up virtual environment in $venvPath ..."); - if (!file_exists($venvPath)) { - // 安装虚拟 - $this->execWithProgress("python3 -m venv $venvPath"); - $this->execWithProgress("source $venvPath/bin/activate"); - // 软链python-config - $pythonConfigPath = "$venvPath/bin/python-config"; - $this->execWithProgress("ln -s /usr/bin/python-config $pythonConfigPath"); - // 软链python-include - $this->execWithProgress("rm -rf $venvPath/include/python$this->pythonVersion"); - $this->execWithProgress("ln -s /usr/include/python$this->pythonVersion $venvPath/include"); - $this->output("Virtual environment created. Python-config path: $pythonConfigPath"); - } else { - $this->comment('Virtual environment already exists.'); - } - } + $config = new Config(); + $pythonSourceUrl = $config->get('python.source-url', 'https://github.com/python/cpython.git'); + $config->set('python.source-url', $this->consoleIO?->ask( + "Please enter the Python source code URL to install (default: $pythonSourceUrl).", + $pythonSourceUrl + )); - return $this->success('Python installation complete.'); - } + $pythonVersion = $config->get('python.install-version', 'latest'); + $config->set('python.install-version', $this->consoleIO?->ask( + "Please enter the Python version to install (default: $pythonVersion).", + $pythonVersion + )); - private function getPythonInstallCommands(): ?string - { - return match ($this->getSystemType()) { - 'alpine' => 'apk add --no-cache python3 py3-pip python3-dev', - 'redhat' => 'sudo yum install -y python3 python3-venv python3-pip python3-dev', - 'darwin' => 'brew install python3', - 'windows' => null, - default => 'sudo apt-get install -y python3 python3-venv python3-pip python3-dev' - }; - } + $pythonInstallDir = $config->get('python.install-dir', '/usr'); + $config->set('python.install-dir', $this->consoleIO?->ask( + "Please enter the Python installation directory (default: $pythonInstallDir).", + $pythonInstallDir + )); - private function getSystemType(): string - { - return match (true) { - file_exists('/etc/alpine-release') => 'alpine', - file_exists('/etc/centos-release') || - file_exists('/etc/redhat-release') => 'redhat', - stripos(php_uname('s'), 'Darwin') !== false => 'darwin', - stripos(php_uname('s'), 'Windows') !== false => 'windows', - default => 'linux', - }; + try { + $buildTools = new BuildToolsInstaller($config, $this->consoleIO); + $buildTools->install(); + (new PythonInstaller($config, $this->consoleIO))->install(); + if ($this->consoleIO?->ask( + 'Do you need to uninstall the build dependencies? [n,Y]', + false, + questionClass: QuestionHelper::class + )) { + $buildTools->uninstall(); + } + } catch (CommandStopException) { + return $this->consoleIO?->success('Installation stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); + } + return $this->consoleIO?->success('Python installation complete.'); } } diff --git a/tools/src/Phpy/Commands/ScanCommand.php b/tools/src/Phpy/Commands/ScanCommand.php new file mode 100644 index 0000000..8b0d8a9 --- /dev/null +++ b/tools/src/Phpy/Commands/ScanCommand.php @@ -0,0 +1,78 @@ +setName('scan') + ->addArgument('dirs', InputArgument::IS_ARRAY|InputArgument::OPTIONAL, 'Scan directories', []) + ->setDescription('Scan python modules import and dependencies') + ->setHelp( + <<scan command scan python modules import and dependencies. + +PHPy introduces modules through Python-pip, read more at +https://pypi.org/help/ +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + $config = new Config(); + if (!$dirs = $this->consoleIO->getInput()->getArgument('dirs')) { + // find json file + $jsonFile = Application::findConfigFile(function ($file, $cDir, $sDir) { + if ($cDir !== $sDir) { + if (!$this->consoleIO?->ask( + "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", + true, + ConfirmationQuestion::class + )) { + throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); + } + } + System::setcwd($cDir); + }); + if (!$jsonFile) { + throw new CommandFailedException('PHPy could not find a phpy.json file in the project'); + } + $config->load($jsonFile); + } else { + $config->set('config.scan-dirs', $dirs); + } + try { + $moduleInstaller = new ModuleInstaller($config, $this->consoleIO); + // 解析 import依赖 + $moduleInstaller->scan(); + // 安装 + $moduleInstaller->install(); + } catch (CommandStopException $exception) { + return $this->consoleIO?->success($exception->getMessage() ?: 'Scan stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); + } + return $this->consoleIO?->error('Scan failed.'); + } +} diff --git a/tools/src/Phpy/Commands/ScanImport.php b/tools/src/Phpy/Commands/ScanImport.php index d2ace3a..f1681f5 100644 --- a/tools/src/Phpy/Commands/ScanImport.php +++ b/tools/src/Phpy/Commands/ScanImport.php @@ -1,16 +1,22 @@ output('Scan the PHP code in the path directory to see what Python modules are imported ...'); - $srcPath = realpath($this->input->getArgument('path')); + $this->consoleIO?->output('Scan the PHP code in the path directory to see what Python modules are imported ...'); + $srcPath = realpath($this->consoleIO?->getInput()->getArgument('path')); if (!is_dir($srcPath)) { - return $this->error('The path is not a directory.'); - } - - $files = $this->findPhpFiles($srcPath); - $packages = []; - foreach ($files as $file) { - $this->output->writeln('Scanning ' . str_replace($srcPath . '/', '', $file) . ''); - $packages = array_merge($packages, PackageCollector::parseFile($file)); + return $this->consoleIO?->error('The path is not a directory.'); } - $packages = array_unique($packages); - if (!$packages) { - $this->output('No Python modules found.'); - return 0; - } - - $packages = array_filter($packages, function ($module) { - return !PythonMetadata::isStdLibrary($module); - }); - $this->output('Found Python modules: ' . implode(', ', array_map(function ($module) { - return "$module"; - }, $packages))); - - $helper = new QuestionHelper(); - $question = new ConfirmationQuestion("[?] Should the dependent packages be written into requirements.txt (yes/no)?: \n", false); - if ($helper->ask($this->getInput(), $this->getOutput(), $question)) { - $pipPackages = array_unique(array_filter(array_map(function ($module) { - return PythonMetadata::getPipPackage($module); - }, $packages), fn($package) => $package !== null)); + try { + $config = new Config(); + $config->set('config.scan-dirs', [$srcPath]); + $moduleInstaller = new ModuleInstaller($config, $this->consoleIO); + $moduleInstaller->scan(); - $requirementsFile = 'requirements.txt'; - - $original = file_get_contents($requirementsFile); - $lines = explode("\n", $original); - foreach ($lines as $line) { - [$_package] = explode('==', $line); - if (in_array($_package, $pipPackages)) { - unset($pipPackages[array_search($_package, $pipPackages)]); + if ($config->get('modules')) { + $buildTools = new BuildToolsInstaller($config, $this->consoleIO); + if ($this->consoleIO?->ask( + 'Do you want to install the dependent packages? [Y,n]', + true, + questionClass: ConfirmationQuestion::class + )) { + $buildTools->install(); + $moduleInstaller->install(); + } + if ($this->consoleIO?->ask( + 'Do you need to uninstall the build dependencies? [n,Y]', + false, + questionClass: ConfirmationQuestion::class + )) { + $buildTools->uninstall(); } } - - $fp = fopen($requirementsFile, 'a'); - fwrite($fp, "\n" . implode("\n", $pipPackages)); - fclose($fp); - - $this->success("The dependent packages have been written to $requirementsFile."); - - if ($helper->ask($this->getInput(), $this->getOutput(), new ConfirmationQuestion("[?] Do you want to install the dependent packages (yes/no)?: \n", false))) { - $this->output('Installing dependent packages ...'); - system('pip install -r ' . $requirementsFile); - return $this->success('The dependent packages have been installed.'); - } + } catch (CommandStopException) { + return $this->consoleIO?->success('Stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); } - return 0; + return $this->consoleIO?->success('Scan import complete.'); } } diff --git a/tools/src/Phpy/Commands/ShowCommand.php b/tools/src/Phpy/Commands/ShowCommand.php index 4e46981..8df96a4 100644 --- a/tools/src/Phpy/Commands/ShowCommand.php +++ b/tools/src/Phpy/Commands/ShowCommand.php @@ -32,18 +32,18 @@ protected function configure(): void protected function handler(): int { $this->consoleIO->output('Python-env: '); - $this->process->pythonExec('--version'); - $this->process->pipExec('--version'); + $this->process->executePython('--version', subOutput: true); + $this->process->executePip('--version', subOutput: true); $this->consoleIO->output('Python-includes: '); - $this->process->pythonConfigExec('--includes'); + $this->process->executePythonConfig('--includes', subOutput: true); if ($module = $this->consoleIO->getInput()->getArgument('module')) { $this->consoleIO->output("Python-module [$module]: "); - $this->process->pipExec("show $module"); + $resultCode = $this->process->executePip("show $module", subOutput: true); } else { $this->consoleIO->output('Python-modules: '); - $this->process->pipExec('list'); + $resultCode = $this->process->executePip('list', subOutput: true); } - return 0; + return $resultCode; } } diff --git a/tools/src/Phpy/Commands/UpdateCommand.php b/tools/src/Phpy/Commands/UpdateCommand.php index 72f5bf7..95a1bd4 100644 --- a/tools/src/Phpy/Commands/UpdateCommand.php +++ b/tools/src/Phpy/Commands/UpdateCommand.php @@ -58,10 +58,13 @@ protected function handler(): int System::setcwd($cDir); }); if (!$jsonFile) { - $sDir = System::getcwd(); throw new CommandFailedException('PHPy could not find a phpy.json file in the project'); } - $config = new Config($jsonFile); + // 尝试读取lock + $lockFile = Application::getLockFile(System::getcwd()); + $config = new Config($lockFile ?: $jsonFile); + // build tools + (new BuildToolsInstaller($config, $this->consoleIO))->install(); // update python env if (!$this->consoleIO?->getInput()->getOption('skip-env')) { (new PythonInstaller($config, $this->consoleIO))->upgrade(); @@ -72,12 +75,9 @@ protected function handler(): int } // install modules if (!$this->consoleIO?->getInput()->getOption('skip-module')) { - // build tools - (new BuildToolsInstaller($config, $this->consoleIO))->upgrade(); // module (new ModuleInstaller($config, $this->consoleIO))->upgrade(); } - $config->set('hash', hash_file('SHA-256', $jsonFile)); Application::setLockFile(System::getcwd(), $config->all()); } catch (CommandStopException) { return $this->consoleIO?->success('Installation stop.'); diff --git a/tools/src/Phpy/Config.php b/tools/src/Phpy/Config.php index 30286a6..d707ed9 100644 --- a/tools/src/Phpy/Config.php +++ b/tools/src/Phpy/Config.php @@ -15,6 +15,7 @@ class Config protected array $config = [ 'config' => [ 'cache-dir' => '~/.cache/phpy', + 'scan-dirs' => [], 'pip-index-url' => '' ], 'python' => [ @@ -36,18 +37,35 @@ class Config 'modules' => [] ]; + /** + * @return string + */ + public function __toString(): string + { + $this->config['modules'] = $this->config['modules'] ?: new \stdClass(); + return json_encode($this->config, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + /** * @param string|null $file */ public function __construct(?string $file = null) { - $config = []; if ($file) { - try { - $config = json_decode(System::getFileContent($file), true, flags: JSON_THROW_ON_ERROR); - } catch (\Throwable) {} + $this->load($file); } - $this->config = array_replace_recursive($this->config, $config); + } + + /** + * @param string $file + * @return void + */ + public function load(string $file): void + { + try { + $config = json_decode(System::getFileContent($file), true, flags: JSON_THROW_ON_ERROR); + } catch (\Throwable) {} + $this->config = array_replace_recursive($this->config, $config ?? []); } /** diff --git a/tools/src/Phpy/ConsoleIO.php b/tools/src/Phpy/ConsoleIO.php index 750b556..00e12ac 100644 --- a/tools/src/Phpy/ConsoleIO.php +++ b/tools/src/Phpy/ConsoleIO.php @@ -10,8 +10,10 @@ use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; class ConsoleIO { @@ -37,6 +39,9 @@ public function __construct( $this->output = $output; $this->helperSet = $helperSet; $this->extra = $extra; + $this->getOutput() + ->getFormatter() + ->setStyle('sub-output', new OutputFormatterStyle('gray', null)); } /** @@ -142,10 +147,7 @@ public function ask(string $message, mixed $default = null, string $tag = '[?]', public function subOutput(string $message, string $tag = '[>]'): void { $this->getOutput() - ->getFormatter() - ->setStyle('sub-output', new OutputFormatterStyle('gray', null)); - $this->getOutput() - ->writeln("$tag $message"); + ->writeln(" $tag $message"); } /** @@ -164,11 +166,12 @@ public function output(string $message, string $tag = '[>]'): void * 输出info * * @param string $message - * @return void + * @return int */ - public function comment(string $message): void + public function comment(string $message): int { $this->getOutput()->writeln("[i] $message"); + return Command::SUCCESS; } /** @@ -179,6 +182,11 @@ public function comment(string $message): void */ public function error(string $message): int { + $output = $this->getOutput(); + if ($output instanceof ConsoleOutput) { + $output->getErrorOutput()->writeln("[×] $message"); + return Command::FAILURE; + } $this->getOutput()->writeln("[×] $message"); return Command::FAILURE; } diff --git a/tools/src/Phpy/Helpers/PackageCollector.php b/tools/src/Phpy/Helpers/PackageCollector.php new file mode 100644 index 0000000..64d2e9f --- /dev/null +++ b/tools/src/Phpy/Helpers/PackageCollector.php @@ -0,0 +1,70 @@ +args[$index]) && $node->args[$index]->value instanceof Node\Scalar\String_) { + $this->packages[] = $node->args[$index]->value->value; + } + }; + switch (true) { + // 静态方法调用 + case ($node instanceof Node\Expr\StaticCall): + if ($node->class instanceof Node\Name && strval($node->class) === 'PyCore' && strval($node->name) === 'import') { + $foundPackageFn($node); + } + break; + // 函数调用 + case ($node instanceof Node\Expr\FuncCall): + if ($node->name instanceof Node\Name && strtolower($node->name) === 'pyimport') { + $foundPackageFn($node); + } + break; + // 注解 + case ($node instanceof Node\Attribute): + if (strval($node->name) === 'PyInherit') { + $foundPackageFn($node, 1); // 捕获第二个参数 + } elseif (strval($node->name) === 'PyImport') { + $foundPackageFn($node); // 捕获第一个参数 + } + break; + + } + } + + public function getPackages(): array + { + return array_unique($this->packages); // 去重 + } + + static function parseFile($filePath): array + { + $code = file_get_contents($filePath); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $traverser = new NodeTraverser; + + $collector = new PackageCollector(); + $traverser->addVisitor($collector); + + try { + $statements = $parser->parse($code); + $traverser->traverse($statements); + } catch (Error $e) { + echo 'Parse Error: ' . $e->getMessage(); + } + + return $collector->getPackages(); + } +} diff --git a/tools/src/Phpy/Helpers/Process.php b/tools/src/Phpy/Helpers/Process.php index b4cf7a6..56213af 100644 --- a/tools/src/Phpy/Helpers/Process.php +++ b/tools/src/Phpy/Helpers/Process.php @@ -55,101 +55,75 @@ private function debugMode(string $info): void } /** - * @param string $command - * @param null|mixed $lastLine - * @return string|int|bool|null - */ - public function pipExec(string $command, mixed &$lastLine = null): string|int|bool|null - { - $pip = System::pip(); - return $this->execWithProgress("$pip $command", $lastLine); - } - - /** - * @param string $command - * @param null|mixed $lastLine - * @return bool|int|string|null - */ - public function pythonExec(string $command, mixed &$lastLine = null): bool|int|string|null - { - $python = System::python(); - return $this->execWithProgress("$python $command", $lastLine); - } - - /** - * @param string $command - * @param null|mixed $lastLine - * @return bool|int|string|null + * 执行命令 + * + * @param string $command 执行命令 + * @param array|null $output stdout & stderr (结果列表始终为倒序) + * @param bool $subOutput 是否输出到subOutput(输出始终为正序) + * @return int 错误码 -1失败 */ - public function pythonConfigExec(string $command, mixed &$lastLine = null): bool|int|string|null + public function execute(string $command, ?array &$output = null, bool $subOutput = false): int { - $python = System::pythonConfig(); - return $this->execWithProgress("$python $command", $lastLine); + $command = str_ends_with($command, ' 2>&1') ? $command : "$command 2>&1"; + $this->debugMode("execute( $command )"); + $resultCode = -1; + $output = $output === null ? [] : $output; + if (is_resource($handle = popen($command, 'r'))) { + // 逐行读取命令输出 + while (!feof($handle)) { + $line = fgets($handle); + if ($line !== false) { + $line = rtrim($line); + array_unshift($output, $line); + if ($subOutput) { + $this->consoleIO?->subOutput($line); + } + } + } + $resultCode = pclose($handle); + } + return $resultCode; } /** - * 执行命令 + * 执行命令pip * * @param string $command * @param mixed|null $output - * @param int|null $resultCode - * @param bool $ignore 忽略中断 - * @return string|int|bool|null + * @param bool $subOutput + * @return int|null */ - public function exec(string $command, mixed &$output = null, mixed &$resultCode = 0, bool $ignore = false): string|int|bool|null + public function executePip(string $command, ?array &$output = null, bool $subOutput = false): int|null { - $this->debugMode("exec( $command )"); - $lastLine = exec($command, $output, $resultCode); - if ($resultCode !== 0 and !$ignore) { - $this->consoleIO?->error($lastLine); - return $resultCode; - } - $this->debugMode("->> rc: $resultCode | last info: $lastLine"); - return $lastLine; + $pip = System::pip(); + return $this->execute("$pip $command", $output, subOutput: $subOutput); } /** + * 执行命令python + * * @param string $command - * @param int|null $resultCode - * @param bool $ignore - * @return string|int|bool|null + * @param mixed|null $output + * @param bool $subOutput + * @return int|null */ - public function system(string $command, ?int &$resultCode = 0, bool $ignore = false): string|int|bool|null + public function executePython(string $command, ?array &$output = null, bool $subOutput = false): int|null { - $this->debugMode("system( $command )"); - $info = system($command, $resultCode); - if ($resultCode !== 0) { - if (!$ignore) { - $this->consoleIO?->error($info); - return $resultCode; - } - } - $this->debugMode("->> rc: $resultCode | last info: $info"); - return $info; + $python = System::python(); + return $this->execute("$python $command", $output, subOutput: $subOutput); } /** + * 执行命令python-config + * * @param string $command - * @param string|null $lastLine - * @return int resultCode + * @param mixed|null $output + * @param bool $subOutput + * @return int|null */ - public function execWithProgress(string $command, ?string &$lastLine = null): int + public function executePythonConfig(string $command, ?array &$output = null, bool $subOutput = false): int|null { - $this->debugMode("execWithProgress( $command )"); - $process = popen($command, 'r'); - while (!feof($process)) { - $line = fgets($process); - if ($line === false) { - break; - } else { - $lastLine = trim($line); - if ($lastLine) { - $this->consoleIO?->subOutput($lastLine); - } - } - usleep(1000); - } - $this->debugMode("> rc: null | last info: $lastLine"); - return pclose($process); + $python = System::pythonConfig(); + return $this->execute("$python $command", $output, subOutput: $subOutput); } } diff --git a/tools/src/PythonMetadata.php b/tools/src/Phpy/Helpers/PythonMetadata.php similarity index 98% rename from tools/src/PythonMetadata.php rename to tools/src/Phpy/Helpers/PythonMetadata.php index 7038a61..8c673b6 100644 --- a/tools/src/PythonMetadata.php +++ b/tools/src/Phpy/Helpers/PythonMetadata.php @@ -1,6 +1,6 @@ process->execWithProgress( - "$command" + if ($this->process->execute( + "$command", subOutput: true ) !== 0) { throw new CommandFailedException('Error installing dependency tools.'); } @@ -64,8 +64,8 @@ public function uninstall(): void false, questionClass: ConfirmationQuestion::class )) { - $this->process->execWithProgress( - "$command" + $this->process->execute( + "$command", subOutput: true ); } } diff --git a/tools/src/Phpy/Installers/ModuleInstaller.php b/tools/src/Phpy/Installers/ModuleInstaller.php index 0fdc065..a61bf17 100644 --- a/tools/src/Phpy/Installers/ModuleInstaller.php +++ b/tools/src/Phpy/Installers/ModuleInstaller.php @@ -9,8 +9,14 @@ use PhpyTool\Phpy\Config; use PhpyTool\Phpy\ConsoleIO; use PhpyTool\Phpy\Exceptions\CommandFailedException; +use PhpyTool\Phpy\Exceptions\CommandStopException; +use PhpyTool\Phpy\Helpers\PackageCollector; use PhpyTool\Phpy\Helpers\Process; +use PhpyTool\Phpy\Helpers\PythonMetadata; use PhpyTool\Phpy\Helpers\System; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use Symfony\Component\Console\Question\ConfirmationQuestion; class ModuleInstaller implements InstallerInterface { @@ -40,7 +46,7 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) $this->config = $config; $this->consoleIO = $consoleIO; $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); - if (!$config->get('module')) { + if (!$config->get('modules')) { $this->skipInfo = 'Module not configured. Skip install.'; return; } @@ -52,118 +58,138 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) * @param string $module * @return array|null */ - protected function moduleVersions(string $module): ?array + public function moduleVersions(string $module): ?array { - $res = $this->process->pipExec("index versions $module"); - if (!str_contains($res, 'ERROR')) { - // 解析 pip 输出,获取模块的可用版本 - preg_match_all('/Available versions: (.+)/', $res, $matches); - return explode(', ', $matches[1][0] ?? ''); + $resultCode = $this->process->executePip("index versions $module", output: $output); + if ($resultCode === 0) { + $res = []; + foreach ($output as $line) { + if (str_starts_with($line, 'Available versions:')) { + // 解析 pip 输出,获取模块的可用版本 + preg_match_all('/Available versions: (.+)/', $line, $matches); + $res = explode(', ', $matches[1][0] ?? ''); + } + } + return $res; } return null; } - /** @inheritdoc */ - public function install(): void + /** + * @return void + */ + public function scan(): void { - $modules = $this->config->get('modules', []); - $installModules = []; - foreach ($modules as $module => $versionConstraint) { - // 查询 pip 库中的模块版本 - if (!$availableVersions = $this->moduleVersions($module)) { - $this->consoleIO?->output(<<$module not found in pip. + $dirs = $this->config->get('config.scan-dirs'); + if (!$dirs) { + throw new CommandStopException('Nothing to scan'); + } + $packages = []; + foreach ($dirs as $dir) { -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Install failed.'); + if (!is_dir($dir = System::getcwd() . $dir)) { + continue; } - // 检查版本是否满足约束 - $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); - if (!$satisfyingVersions) { - $this->consoleIO?->output(<<$module version $versionConstraint> not found in pip. + $files = $this->findPhpFiles(realpath($dir)); -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Install failed.'); + foreach ($files as $file) { + $this->consoleIO?->output("Scanning $file"); + $scannedPackages = PackageCollector::parseFile($file); + foreach ($scannedPackages as $package) { + $this->consoleIO?->subOutput("-- scanned: $package"); + } + $packages = array_merge($packages, $scannedPackages); } - // 选择满足约束的版本 - $installModules[$module] = Semver::rsort($satisfyingVersions); } + $packages = array_unique($packages); + $modules = []; + foreach ($packages as $key => $package) { + $package = explode('.', $package)[0]; + if (PythonMetadata::isStdLibrary($package)) { + continue; + } + if ($availableVersions = $this->moduleVersions($package)) { + $modules[$package] = Semver::rsort($availableVersions)[0]; + } + } + if ($modules) { + $count = count($modules); + $this->consoleIO?->output("Installs: $count"); + foreach ($modules as $module => $version) { + $this->consoleIO?->subOutput("-- $module - $version"); + } + } + if (!$this->consoleIO?->ask( + "Do you want to install these modules? [Y,n]", + true, + ConfirmationQuestion::class + )) { + throw new CommandStopException('PHPy will not install any modules'); + } + $this->config->set('modules', array_merge($this->config->get('modules', []), $modules)); + } + + /** @inheritdoc */ + public function install(): void + { + $modules = $this->config->get('modules', []); + $vendorModules = $this->config->get('vendor-modules', []); + $phpyHash = $this->config->get('phpy-hash'); + $composerHash = $this->config->get('composer-hash'); + + // 如果存在 phpy-hash 和 composer-hash, 则为install + if ($phpyHash and $composerHash) { + $installModules = array_merge($modules, $vendorModules); + } + // 不存在,则为update + else { + $this->config->set('phpy-hash', hash_file('SHA256', System::getcwd() . '/phpy.json')); + $this->config->set('composer-hash', hash_file('SHA256', System::getcwd() . '/composer.json')); + $localModules = []; + foreach ($modules as $module => $versionConstraint) { + $localModules[$module] = $this->satisfyingVersions($module, $this->availableVersions($module), $versionConstraint); + } - // 没有hash,说明是json文件安装,则扫描vendor - if (!$this->config->get('hash')) { // vendor - $vendorModules = []; - Application::getVendorConfigFiles(function ($organization, $package, $configFilePath) use (&$vendorModules) { + $vendors = []; + Application::getVendorConfigFiles(function ($organization, $package, $configFilePath) use (&$vendors) { $config = new Config($configFilePath); $modules = $config->get('modules', []); foreach ($modules as $module => $versionConstraint) { - $vendorModules[$module][$versionConstraint] = [ + $vendors[$module][$versionConstraint] = [ 'organization' => $organization, 'package' => $package, ]; } }); - foreach ($vendorModules as $module => $item) { - // others - if (!isset($installModules[$module])) { - // 查询 pip 库中的模块版本 - if (!$availableVersions = $this->moduleVersions($module)) { - $this->consoleIO?->output(<<$module not found in pip. + $this->config->set('vendors', $vendors); -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Install failed.'); - } - } - // exits - else { - $availableVersions = $installModules[$module]; - } + $vendorModules = []; + foreach ($vendors as $module => $item) { + $availableVersions = ($local = isset($localModules[$module])) + ? $localModules[$module] + : $this->availableVersions($module); + // 循环检查所有组织/包的版本约束 foreach ($item as $versionConstraint => $info) { - // 检查版本是否满足约束 - $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); - if (!$satisfyingVersions) { - $this->consoleIO?->output(<<{$info['organization']}/{$info['package']} -- -Python module $module version-constraint $versionConstraint> not found in pip. - -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Install failed.'); + $availableVersions = $this->satisfyingVersions($module, $availableVersions, $versionConstraint, $info); + } + if ($availableVersions) { + if (!$local) { + $vendorModules[$module] = $availableVersions[0]; + } else { + $localModules[$module] = $availableVersions[0]; } - $availableVersions = Semver::rsort($satisfyingVersions); } - $installModules[$module] = $availableVersions; } + $this->config->set('modules', $localModules); + $this->config->set('vendor-modules', $vendorModules); + $installModules = array_merge($localModules, $vendorModules); } + if ($installModules) { - // 生成 requirements.txt 且安装 - $installModulesContent = ''; - foreach ($installModules as $module => $versions) { - $this->config->set("modules.$module", $version = $versions[0]); - $installModulesContent .= "$module==$version\n"; - } - System::putFileContent($requirementsFile = System::getcwd() . '/requirements.txt', $installModulesContent, cache: false); - $this->consoleIO->subOutput(<<config->get('config.pip-index-url')) { - $this->process->pipExec("config set global.index-url $pipGlobalIndex"); - } - if ($this->process->pipExec("install -r $requirementsFile") !== 0) { - throw new CommandFailedException('Install failed.'); - } + $this->pipModulesInstall($installModules); } else { - $this->consoleIO->output('No modules to install.'); + $this->consoleIO->output('No modules.'); } } @@ -175,105 +201,121 @@ public function uninstall(): void /** @inheritdoc */ public function upgrade(): void { - $modules = $this->config->get('modules', []); - $this->config->set('local-modules', $modules); - $installModules = []; - foreach ($modules as $module => $versionConstraint) { - // 查询 pip 库中的模块版本 - if (!$availableVersions = $this->moduleVersions($module)) { - $this->consoleIO?->output(<<$module not found in pip. - -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Update failed.'); + if ($hash = $this->config->get('phpy-hash')) { + $phpyHash = hash_file('SHA256', System::getcwd() . '/phpy.json'); + if ($phpyHash !== $hash) { + $this->config->set('phpy-hash', null); } - // 检查版本是否满足约束 - $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); - if (!$satisfyingVersions) { - $this->consoleIO?->output(<<$module version-constraint $versionConstraint> not found in pip. - -Read more about https://pypi.org/search -EOT - ); - throw new CommandFailedException('Update failed.'); + } + if ($hash = $this->config->get('composer-hash')) { + $composerHash = hash_file('SHA256', System::getcwd() . '/composer.json'); + if ($composerHash !== $hash) { + $this->config->set('composer-hash', null); } - // 选择满足约束的版本 - $installModules[$module] = Semver::rsort($satisfyingVersions); } - // vendor - $vendorModules = []; - Application::getVendorConfigFiles(function ($organization, $package, $configFilePath) use (&$vendorModules) { - $config = new Config($configFilePath); - $modules = $config->get('modules', []); - foreach ($modules as $module => $versionConstraint) { - $vendorModules[$module][$versionConstraint] = [ - 'organization' => $organization, - 'package' => $package, - ]; + $this->install(); + } + + /** @inheritdoc */ + public function clearCache(): void + { + } + + + /** + * 查找php文件 + * + * @param $directory + * @return array + */ + private function findPhpFiles($directory): array + { + $phpFiles = []; + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); + foreach ($iterator as $file) { + if ($file->isFile() && pathinfo($file->getFilename(), PATHINFO_EXTENSION) === 'php') { + $phpFiles[] = $file->getPathname(); } - }); - foreach ($vendorModules as $module => $item) { - // others - if (!isset($installModules[$module])) { - // 查询 pip 库中的模块版本 - if (!$availableVersions = $this->moduleVersions($module)) { - $this->consoleIO?->output(<<$module not found in pip. + } + return $phpFiles; + } + + /** + * 获取可用的Python模块版本 + * + * @param string $module + * @param array $availableVersions + * @param string $versionConstraint + * @param array $info + * @return array + */ + private function satisfyingVersions(string $module, array $availableVersions, string $versionConstraint, array $info = []): array + { + $organization = $info['organization'] ?? null; + $package = $info['package'] ?? null; + $msg = ($organization and $package) + ? "Package {$info['organization']}/{$info['package']}" + : "Local"; + // 检查版本是否满足约束 + $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); + if (!$satisfyingVersions) { + $this->consoleIO?->output(<<$module-$versionConstraint> not found in pip. Read more about https://pypi.org/search EOT - ); - throw new CommandFailedException('Update failed.'); - } - } - // exits - else { - $availableVersions = $installModules[$module]; - } - foreach ($item as $versionConstraint => $info) { - // 检查版本是否满足约束 - $satisfyingVersions = Semver::satisfiedBy($availableVersions, $versionConstraint); - if (!$satisfyingVersions) { - $this->consoleIO?->output(<<{$info['organization']}/{$info['package']} -- -Python module $module version-constraint $versionConstraint> not found in pip. + ); + throw new CommandFailedException('Failed.'); + } + // 选择满足约束的版本列表 + return Semver::rsort($satisfyingVersions); + } + + /** + * 返回pip可用版本列表 + * + * @param string $module + * @return array + */ + public function availableVersions(string $module): array + { + // 查询 pip 库中的模块版本 + if (!$availableVersions = $this->moduleVersions($module)) { + $this->consoleIO?->output(<<$module not found in pip. Read more about https://pypi.org/search EOT - ); - throw new CommandFailedException('Update failed.'); - } - $availableVersions = Semver::rsort($satisfyingVersions); - } - $installModules[$module] = $availableVersions; + ); + throw new CommandFailedException('Failed.'); } - if ($installModules) { - // 生成 requirements.txt 且安装 - $installModulesContent = ''; - foreach ($installModules as $module => $versions) { - $this->config->set("modules.$module", $version = $versions[0]); - $installModulesContent .= "$module==$version\n"; - } - System::putFileContent($requirementsFile = System::getcwd() . '/requirements.txt', $installModulesContent, cache: false); - $this->consoleIO->subOutput(<<config->get('config.pip-index-url')) { - $this->process->pipExec("config set global.index-url $pipGlobalIndex"); - } - if ($this->process->pipExec("install -r $requirementsFile") !== 0) { - throw new CommandFailedException('Update failed.'); - } - } - $this->config->set('vendor-modules', $vendorModules); + return $availableVersions; } - /** @inheritdoc */ - public function clearCache(): void + /** + * pip安装模块 + * + * @param array $modules + * @return void + */ + private function pipModulesInstall(array $modules): void { + // 生成 requirements.txt 且安装 + $installModulesContent = ''; + foreach ($modules as $module => $version) { + $installModulesContent .= "$module==$version\n"; + } + System::putFileContent($requirementsFile = System::getcwd() . '/requirements.txt', $installModulesContent, cache: false); + $this->consoleIO->subOutput(<<config->get('config.pip-index-url')) { + $this->process->executePip("config set global.index-url $pipGlobalIndex"); + } + if ($this->process->executePip("install -r $requirementsFile", subOutput: true) !== 0) { + throw new CommandFailedException('Failed.'); + } } } diff --git a/tools/src/Phpy/Installers/PhpyInstaller.php b/tools/src/Phpy/Installers/PhpyInstaller.php index be042ac..aeb8b56 100644 --- a/tools/src/Phpy/Installers/PhpyInstaller.php +++ b/tools/src/Phpy/Installers/PhpyInstaller.php @@ -63,8 +63,8 @@ public function install(): void $sourceDir = "$cacheDir/phpy-$version"; if (!file_exists($sourceDir)) { $this->consoleIO?->output('PHPy-source Downloading ...'); - if ($this->process->execWithProgress( - "git clone --depth 1 $versionOpt $url $sourceDir" + if ($this->process->execute( + "git clone --depth 1 $versionOpt $url $sourceDir", subOutput: true ) !== 0) { throw new CommandFailedException('Error downloading PHPy-source.'); } @@ -79,8 +79,9 @@ public function install(): void $phpIniPath = $this->config->get('phpy.ini-path'); $iniCmd = $phpIniPath ? "&& echo 'extension=phpy.so' > $phpIniPath" : ''; if ( - $this->process->execWithProgress( - "cd $sourceDir && phpize && ./configure $phpyInstallConfigure && make clean && make && make install $iniCmd" + $this->process->execute( + "cd $sourceDir && phpize && ./configure $phpyInstallConfigure && make clean && make && make install $iniCmd", + subOutput: true ) !== 0 ) { throw new CommandFailedException('Error building and installing PHPy extension.'); @@ -92,7 +93,7 @@ public function uninstall(): void { $phpIniPath = $this->config->get('phpy.ini-path'); if (file_exists($phpIniPath)) { - $this->process->exec("rm $phpIniPath"); + $this->process->execute("rm $phpIniPath", subOutput: true); } } @@ -106,8 +107,8 @@ public function upgrade(): void public function clearCache(): void { $cacheDir = $this->config->get('config.cache-dir'); - if ($this->process->execWithProgress( - "rm -rf $cacheDir/phpy-*" + if ($this->process->execute( + "rm -rf $cacheDir/phpy-*", subOutput: true ) !== 0) { throw new CommandFailedException('Error clearing PHPy cache.'); } diff --git a/tools/src/Phpy/Installers/PythonInstaller.php b/tools/src/Phpy/Installers/PythonInstaller.php index fcbf73d..133b15d 100644 --- a/tools/src/Phpy/Installers/PythonInstaller.php +++ b/tools/src/Phpy/Installers/PythonInstaller.php @@ -76,8 +76,8 @@ public function install(): void $sourceDir = "$cacheDir/python-$version"; if (!file_exists($sourceDir)) { $this->consoleIO?->output('CPython-source Downloading ...'); - if ($this->process->execWithProgress( - "git clone --depth 1 $versionOpt $url $sourceDir" + if ($this->process->execute( + "git clone --depth 1 $versionOpt $url $sourceDir", subOutput: true ) !== 0) { throw new CommandFailedException('Error downloading Python.'); } @@ -90,8 +90,9 @@ public function install(): void $pythonInstallConfigure = implode(' ', $pythonInstallConfigure); $this->consoleIO?->output("Building and installing Python-$version..."); if ( - $this->process->execWithProgress( - "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install" + $this->process->execute( + "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install", + subOutput: true ) !== 0 ) { throw new CommandFailedException("Error building and installing Python-$version."); @@ -102,18 +103,21 @@ public function install(): void $pip = $installDir . '/bin/pip'; $cwd = System::getcwd(); // 虚拟环境 - if (!file_exists($venvPath = "$cwd/py-vendor/.venv")) { + if (!file_exists($venvPath = "$cwd/py-vendor")) { // 安装虚拟 - $this->process->execWithProgress("$python -m venv $venvPath"); - $this->process->execWithProgress("source $venvPath/bin/activate"); + $this->process->execute("$python -m venv $venvPath", subOutput: true); + $this->process->execute("source $venvPath/bin/activate", subOutput: true); // 软链python-config $pythonConfigPath = "$venvPath/bin/python-config"; - $this->process->execWithProgress("ln -s $installDir/bin/python-config $pythonConfigPath"); + $this->process->execute("ln -s $installDir/bin/python-config $pythonConfigPath", subOutput: true); // 软链python-include - $this->process->execWithProgress("rm -rf $venvPath/include/python"); - $this->process->execWithProgress("ln -s $installDir/include/python $venvPath/include"); + $this->process->execute("rm -rf $venvPath/include/python", subOutput: true); + $this->process->execute("ln -s $installDir/include/python $venvPath/include", subOutput: true); // 设置环境 - $this->process->execWithProgress("echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command"); + $this->process->execute( + "echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command", + subOutput: true + ); } } @@ -125,12 +129,12 @@ public function uninstall(): void // 卸载源码 $sourceDir = "$cacheDir/python-$version"; if (file_exists($sourceDir)) { - $this->process->exec("rm -rf $sourceDir"); + $this->process->execute("rm -rf $sourceDir", subOutput: true); } // 卸载虚拟环境 $cwd = System::getcwd(); if (file_exists($venvPath = "$cwd/py-vendor/.venv")) { - $this->process->exec("rm -rf $venvPath"); + $this->process->execute("rm -rf $venvPath", subOutput: true); } } @@ -147,11 +151,11 @@ public function upgrade(): void // 下载源码 $sourceDir = "$cacheDir/python-$version"; if (file_exists($sourceDir)) { - $this->process->exec("rm -rf $sourceDir"); + $this->process->execute("rm -rf $sourceDir", subOutput: true); } $this->consoleIO?->output('CPython-source Downloading ...'); - if ($this->process->execWithProgress( - "git clone --depth 1 $versionOpt $url $sourceDir" + if ($this->process->execute( + "git clone --depth 1 $versionOpt $url $sourceDir", subOutput: true ) !== 0) { throw new CommandFailedException('Error downloading Python.'); } @@ -163,8 +167,9 @@ public function upgrade(): void $pythonInstallConfigure = implode(' ', $pythonInstallConfigure); $this->consoleIO?->output("Building and installing Python-$version..."); if ( - $this->process->execWithProgress( - "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install" + $this->process->execute( + "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install", + subOutput: true ) !== 0 ) { throw new CommandFailedException("Error building and installing Python-$version."); @@ -176,27 +181,30 @@ public function upgrade(): void $cwd = System::getcwd(); // 虚拟环境 if (file_exists($venvPath = "$cwd/py-vendor/.venv")) { - $this->process->exec("rm -rf $venvPath"); + $this->process->execute("rm -rf $venvPath", subOutput: true); } // 安装虚拟 - $this->process->execWithProgress("$python -m venv $venvPath"); - $this->process->execWithProgress("source $venvPath/bin/activate"); + $this->process->execute("$python -m venv $venvPath", subOutput: true); + $this->process->execute("source $venvPath/bin/activate", subOutput: true); // 软链python-config $pythonConfigPath = "$venvPath/bin/python-config"; - $this->process->execWithProgress("ln -s $installDir/bin/python-config $pythonConfigPath"); + $this->process->execute("ln -s $installDir/bin/python-config $pythonConfigPath", subOutput: true); // 软链python-include - $this->process->execWithProgress("rm -rf $venvPath/include/python"); - $this->process->execWithProgress("ln -s $installDir/include/python $venvPath/include"); + $this->process->execute("rm -rf $venvPath/include/python", subOutput: true); + $this->process->execute("ln -s $installDir/include/python $venvPath/include", subOutput: true); // 设置环境 - $this->process->execWithProgress("echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command"); + $this->process->execute( + "echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command", + subOutput: true + ); } /** @inheritdoc */ public function clearCache(): void { $cacheDir = $this->config->get('config.cache-dir'); - if ($this->process->execWithProgress( - "rm -rf $cacheDir/python-*" + if ($this->process->execute( + "rm -rf $cacheDir/python-*", subOutput: true ) !== 0) { throw new CommandFailedException('Error clearing Python cache.'); } From 52b2c09e82eff13673147c5d7e1c11fcb964ac8d Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Mon, 24 Mar 2025 10:42:16 +0800 Subject: [PATCH 3/6] support php-81 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 08103a5..fdfdf56 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "require-dev": { "phpunit/phpunit": "^10.4", "friendsofphp/php-cs-fixer": "^3.40", - "symfony/var-dumper": "^7.0" + "symfony/var-dumper": "^6.0 | ^7.0" }, "autoload": { "psr-4": { From 41103189df1942a698523b84aca78586e9487194 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Tue, 25 Mar 2025 21:25:15 +0800 Subject: [PATCH 4/6] fix bugs --- tools/src/Phpy/Application.php | 2 + tools/src/Phpy/Commands/ClearCacheCommand.php | 72 +++++++++++++++++++ tools/src/Phpy/Commands/InstallCommand.php | 3 +- tools/src/Phpy/Commands/PhpyInstall.php | 2 +- tools/src/Phpy/Commands/ScanCommand.php | 2 +- tools/src/Phpy/Commands/UpdateCommand.php | 2 +- tools/src/Phpy/Helpers/Process.php | 10 +-- tools/src/Phpy/Installers/PhpyInstaller.php | 8 ++- tools/src/Phpy/Installers/PythonInstaller.php | 34 ++++++--- 9 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 tools/src/Phpy/Commands/ClearCacheCommand.php diff --git a/tools/src/Phpy/Application.php b/tools/src/Phpy/Application.php index 12ca8bb..795cf9e 100644 --- a/tools/src/Phpy/Application.php +++ b/tools/src/Phpy/Application.php @@ -5,6 +5,7 @@ namespace PhpyTool\Phpy; use Closure; +use PhpyTool\Phpy\Commands\ClearCacheCommand; use PhpyTool\Phpy\Commands\InitConfigCommand; use PhpyTool\Phpy\Commands\InstallCommand; use PhpyTool\Phpy\Commands\PhpyInstall; @@ -44,6 +45,7 @@ public function __construct() new PhpyInstall(), new PythonInstall(), new PipMirrorConfig(), + new ClearCacheCommand(), ]); } diff --git a/tools/src/Phpy/Commands/ClearCacheCommand.php b/tools/src/Phpy/Commands/ClearCacheCommand.php new file mode 100644 index 0000000..5dfa046 --- /dev/null +++ b/tools/src/Phpy/Commands/ClearCacheCommand.php @@ -0,0 +1,72 @@ +setName('clear-cache') + ->setDescription("Clears phpy's Python-source and ext-phpy cache") + ->setHelp( + <<clear-cache command deletes all cached Python-source and ext-phpy from +phpy's cache directory. +EOT + ); + } + + /** @inheritdoc */ + protected function handler(): int + { + try { + // find json file + $jsonFile = Application::findConfigFile(function ($file, $cDir, $sDir) { + if ($cDir !== $sDir) { + if (!$this->consoleIO?->ask( + "No phpy.json in current directory, do you want to use the one at $cDir [Y,n]?", + true, + ConfirmationQuestion::class + )) { + throw new CommandStopException("PHPy could not find a phpy.json file in $sDir"); + } + } + System::setcwd($cDir); + }); + if (!$jsonFile) { + throw new CommandFailedException('PHPy could not find a phpy.json file in the project'); + } + $config = new Config($jsonFile); + // Python + (new PythonInstaller($config, $this->consoleIO))->clearCache(); + // ext-phpy + (new PhpyInstaller($config, $this->consoleIO))->clearCache(); + + } catch (CommandStopException) { + return $this->consoleIO?->success('Clear-cache stop.'); + } catch (CommandSuccessedException $exception) { + return $this->consoleIO?->success($exception->getMessage()); + } catch (CommandFailedException $exception) { + return $this->consoleIO?->error($exception->getMessage()); + } + return $this->consoleIO?->success('Clear-cache completed.'); + } +} diff --git a/tools/src/Phpy/Commands/InstallCommand.php b/tools/src/Phpy/Commands/InstallCommand.php index 153b058..23e8e01 100644 --- a/tools/src/Phpy/Commands/InstallCommand.php +++ b/tools/src/Phpy/Commands/InstallCommand.php @@ -95,6 +95,7 @@ protected function handler(): int if (!$lockFile) { Application::setLockFile(System::getcwd(), $config->all()); } + } catch (CommandStopException) { return $this->consoleIO?->success('Installation stop.'); } catch (CommandSuccessedException $exception) { @@ -102,6 +103,6 @@ protected function handler(): int } catch (CommandFailedException $exception) { return $this->consoleIO?->error($exception->getMessage()); } - return $this->consoleIO?->error('Installation failed.'); + return $this->consoleIO?->success('Installation completed.'); } } diff --git a/tools/src/Phpy/Commands/PhpyInstall.php b/tools/src/Phpy/Commands/PhpyInstall.php index 43e00c1..5b038fa 100644 --- a/tools/src/Phpy/Commands/PhpyInstall.php +++ b/tools/src/Phpy/Commands/PhpyInstall.php @@ -74,6 +74,6 @@ protected function handler(): int return $this->consoleIO?->error($exception->getMessage()); } - return $this->consoleIO?->success('PHPy installation completed successfully. '); + return $this->consoleIO?->success('PHPy installation completed. '); } } diff --git a/tools/src/Phpy/Commands/ScanCommand.php b/tools/src/Phpy/Commands/ScanCommand.php index 8b0d8a9..6ae8b76 100644 --- a/tools/src/Phpy/Commands/ScanCommand.php +++ b/tools/src/Phpy/Commands/ScanCommand.php @@ -73,6 +73,6 @@ protected function handler(): int } catch (CommandFailedException $exception) { return $this->consoleIO?->error($exception->getMessage()); } - return $this->consoleIO?->error('Scan failed.'); + return $this->consoleIO?->success('Scan completed.'); } } diff --git a/tools/src/Phpy/Commands/UpdateCommand.php b/tools/src/Phpy/Commands/UpdateCommand.php index 95a1bd4..8ae63da 100644 --- a/tools/src/Phpy/Commands/UpdateCommand.php +++ b/tools/src/Phpy/Commands/UpdateCommand.php @@ -86,6 +86,6 @@ protected function handler(): int } catch (CommandFailedException $exception) { return $this->consoleIO?->error($exception->getMessage()); } - return $this->consoleIO?->error('Installation failed.'); + return $this->consoleIO?->success('Installation completed.'); } } diff --git a/tools/src/Phpy/Helpers/Process.php b/tools/src/Phpy/Helpers/Process.php index 56213af..44cb7ad 100644 --- a/tools/src/Phpy/Helpers/Process.php +++ b/tools/src/Phpy/Helpers/Process.php @@ -73,10 +73,12 @@ public function execute(string $command, ?array &$output = null, bool $subOutput while (!feof($handle)) { $line = fgets($handle); if ($line !== false) { - $line = rtrim($line); - array_unshift($output, $line); - if ($subOutput) { - $this->consoleIO?->subOutput($line); + $line = $line ? trim($line) : null; + if ($line) { + array_unshift($output, $line); + if ($subOutput) { + $this->consoleIO?->subOutput($line); + } } } } diff --git a/tools/src/Phpy/Installers/PhpyInstaller.php b/tools/src/Phpy/Installers/PhpyInstaller.php index aeb8b56..2ada53e 100644 --- a/tools/src/Phpy/Installers/PhpyInstaller.php +++ b/tools/src/Phpy/Installers/PhpyInstaller.php @@ -56,8 +56,11 @@ public function install(): void return; } $url = $this->config->get('phpy.source-url'); - $version = $this->config->get('phpy.version', 'latest'); + $version = $this->config->get('phpy.install-version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } $versionOpt = ($version === 'latest') ? '' : "--branch $version"; // 下载源码 $sourceDir = "$cacheDir/phpy-$version"; @@ -107,6 +110,9 @@ public function upgrade(): void public function clearCache(): void { $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } if ($this->process->execute( "rm -rf $cacheDir/phpy-*", subOutput: true ) !== 0) { diff --git a/tools/src/Phpy/Installers/PythonInstaller.php b/tools/src/Phpy/Installers/PythonInstaller.php index 133b15d..042a66c 100644 --- a/tools/src/Phpy/Installers/PythonInstaller.php +++ b/tools/src/Phpy/Installers/PythonInstaller.php @@ -65,8 +65,11 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) /** @inheritdoc */ public function install(): void { - $version = $this->config->get('python.version', 'latest'); + $version = $this->config->get('python.install-version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } $installDir = $this->config->get('python.install-dir'); if (!$this->skipInfo) { @@ -91,21 +94,27 @@ public function install(): void $this->consoleIO?->output("Building and installing Python-$version..."); if ( $this->process->execute( - "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make && make install", + "cd $sourceDir && ./configure $pythonInstallConfigure && make clean && make -j$(nproc) && make install", subOutput: true ) !== 0 ) { throw new CommandFailedException("Error building and installing Python-$version."); } + $python = $installDir . '/bin/python'; + $pip = $installDir . '/bin/pip'; + $cwd = System::getcwd(); + // 设置环境 + $this->process->execute( + "echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command", + subOutput: true + ); } - $python = $installDir . '/bin/python'; - $pip = $installDir . '/bin/pip'; $cwd = System::getcwd(); // 虚拟环境 if (!file_exists($venvPath = "$cwd/py-vendor")) { // 安装虚拟 - $this->process->execute("$python -m venv $venvPath", subOutput: true); + $this->process->executePython("-m venv $venvPath", subOutput: true); $this->process->execute("source $venvPath/bin/activate", subOutput: true); // 软链python-config $pythonConfigPath = "$venvPath/bin/python-config"; @@ -113,11 +122,7 @@ public function install(): void // 软链python-include $this->process->execute("rm -rf $venvPath/include/python", subOutput: true); $this->process->execute("ln -s $installDir/include/python $venvPath/include", subOutput: true); - // 设置环境 - $this->process->execute( - "echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command", - subOutput: true - ); + } } @@ -126,6 +131,9 @@ public function uninstall(): void { $version = $this->config->get('python.version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } // 卸载源码 $sourceDir = "$cacheDir/python-$version"; if (file_exists($sourceDir)) { @@ -143,6 +151,9 @@ public function upgrade(): void { $version = $this->config->get('python.version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } $installDir = $this->config->get('python.install-dir'); if (!$this->skipInfo) { @@ -203,6 +214,9 @@ public function upgrade(): void public function clearCache(): void { $cacheDir = $this->config->get('config.cache-dir'); + if (str_starts_with($cacheDir, '~')) { + $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); + } if ($this->process->execute( "rm -rf $cacheDir/python-*", subOutput: true ) !== 0) { From 09196fff3d802de97ee69d65233d4d439987fe44 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 26 Mar 2025 10:35:39 +0800 Subject: [PATCH 5/6] fix bugs & docs update --- docs/cn/README.md | 1 + docs/cn/php/phpy.md | 145 ++++++++++++++++++ phpy.json | 12 +- tools/src/Phpy/Application.php | 6 +- tools/src/Phpy/Commands/InstallCommand.php | 3 + tools/src/Phpy/Config.php | 13 +- tools/src/Phpy/Helpers/System.php | 93 +++++++++-- tools/src/Phpy/Helpers/Version.php | 41 +++++ .../Phpy/Installers/BuildToolsInstaller.php | 42 ++--- tools/src/Phpy/Installers/ModuleInstaller.php | 6 +- tools/src/Phpy/Installers/PhpyInstaller.php | 3 +- tools/src/Phpy/Installers/PythonInstaller.php | 46 ++++-- 12 files changed, 351 insertions(+), 60 deletions(-) create mode 100644 docs/cn/php/phpy.md create mode 100644 tools/src/Phpy/Helpers/Version.php diff --git a/docs/cn/README.md b/docs/cn/README.md index ffde243..23ccca9 100644 --- a/docs/cn/README.md +++ b/docs/cn/README.md @@ -18,6 +18,7 @@ PHP 扩展 * [IDE 提示](php/composer.md) * [Socket API](php/socket.md) * [继承 Python 类](php/inherit.md) +* [PHPy 工具](php/phpy.md) Python 模块 --- diff --git a/docs/cn/php/phpy.md b/docs/cn/php/phpy.md new file mode 100644 index 0000000..a5b3732 --- /dev/null +++ b/docs/cn/php/phpy.md @@ -0,0 +1,145 @@ +# PHPy 管理工具 + +PHPy提供类似`composer`的包管理工具,可以安装、更新、卸载 `Python`环境、`swoole/phpy`拓展及`Python-module`, +对其他`composer`引入的`Python`相关信息和模块进行依赖管理。 + +## 使用 + +通过`composer`安装`swoole/phpy` +```shell +composer require swoole/phpy +``` + +## 文档 + +### 1. 初始化`phpy.json`配置 + +#### 命令: +```shell +./vendor/bin/phpy init-config +``` + +**与`composer`类似,`PHPy`也有自己的配置管理文件`phpy.json`;使用`init-config`命令可以在当前目录下创建`phpy.json`文件。** + +#### 文件内容: +```php +{ + // 全局配置 + "config": { + // 缓存目录 + "cache-dir": "~/.cache/phpy", + // 扫描路径 + "scan-dirs": [ + ], + // pip源 + "pip-index-url": "" + }, + // python 配置 + "python": { + // 源码路径 + "source-url": "https://github.com/python/cpython.git", + // 安装路径 + "install-dir": "/usr", + // 安装版本,不支持latest构建 + "install-version": "v3.13.2", + // 编译参数(建议不要改动) + "install-configure": [ + "--enable-shared", + "--with-system-expat", + "--with-system-ffi", + "--enable-ipv6", + "--enable-loadable-sqlite-extensions", + "--with-computed-gotos", + "--with-ensurepip=install" + ] + }, + // phpy 配置 + "phpy": { + // 源码路径 + "source-url": "https://github.com/swoole/phpy.git", + // 安装路径,支持latest使用master分支进行构建 + "install-version": "latest", + // 编译参数 + "install-configure": [], + // ini文件路径,空字符串为不自动引入php.ini + "ini-path": "/usr/local/etc/php/conf.d/xx-php-ext-phpy.ini" + }, + // 模块配置 + "modules": { + // 例子 + "pandas": "^2.0" + } +} +``` + +### 2. 依赖安装 + +#### 命令: +```shell +./vendor/bin/phpy install +``` +- `install`命令会根据当前项目及`vendor`中引入的所有`composer`包的`phpy.json`配置信息进行安装,安装内容如下: + - 编译构建依赖,详见[BuildToolsInstaller.php](../../../tools/src/Phpy/Installer/BuildToolsInstaller.php) + - 编译安装`Python`环境,详见[PythonInstaller.php](../../../tools/src/Phpy/Installer/PythonInstaller.php) + - 编译安装`phpy`拓展,详见[PhpyInstaller.php](../../../tools/src/Phpy/Installer/PhpyInstaller.php) + - 安装`Python`模块,详见[ModuleInstaller.php](../../../tools/src/Phpy/Installer/ModuleInstaller.php) +- `install`命令会在项目路径下创建`phpy.lock`文件,用于记录安装信息,下次安装时,如果`phpy.lock`文件存在,则不会重复安装。 +- `install`命令默认使用`Python-venv`环境,会在项目路径下创建`py-vendor`目录,用于存储`Python`环境及模块。 +- `install`命令会在项目路径下创建如下文件,**以下文件建议加入项目`.gitignore`文件**: + - `pip.command`:用于提供`executePip`方法标准化执行`pip`命令 + - `python.command`:用于提供`executePython`方法标准化执行`python`命令 + - `phpy.command`:用于提供`executePhpy`方法标准化执行`phpy`命令 + - `phpy.lock`:用于记录安装信息,下次安装时,如果`phpy.lock`文件存在,则不会重复安装 + - `requirements.txt`:用于记录安装的`Python`模块信息,使用标准化`executePip`安装 +- 更多查看`--help` + +### 3. 依赖更新 + +#### 命令: +```shell +./vendor/bin/phpy update +``` + +`update`命令会根据`phpy.json`配置信息进行更新,更多查看`--help` + +### 4. 环境检查 + +#### 命令: +```shell +./vendor/bin/phpy show +``` + +`show`命令会展示当前`Python`环境信息及引入的`Python`模块信息,更多查看`--help` + +```shell +/var/www/test-project # ./vendor/bin/phpy show +[>] Python-env: + [>] Python 3.13.2 + [>] pip 25.0.1 from /var/www/phpy/py-vendor/lib/python3.13/site-packages/pip (python 3.13) +[>] Python-includes: + [>] -I/var/www/phpy/py-vendor/include/python3.13 -I/var/www/phpy/py-vendor/include/python3.13 +[>] Python-modules: + [>] Package Version + [>] ------- ------- + [>] pip 25.0.1 + [>] pyorc 0.10.0 +``` + +### 5. 扫描引入 + +#### 命令: +```shell +./vendor/bin/phpy scan +``` + +`scan`命令会根据`phpy.json`的`config.scan-dirs`扫描所有php文件并检查依赖的`Python-module`, +引入并安装,更多查看`--help` + +### 6. 缓存清除 + +#### 命令: +```shell +./vendor/bin/phpy clear-cache +``` + +`clear-cache`命令会根据`phpy.json`的`config.cache-dir`清除相关缓存,更多查看`--help` diff --git a/phpy.json b/phpy.json index 94e0b7d..13341f1 100644 --- a/phpy.json +++ b/phpy.json @@ -8,11 +8,15 @@ "python": { "source-url": "https://github.com/python/cpython.git", "install-dir": "/usr", - "install-version": "latest", + "install-version": "v3.13.2", "install-configure": [ - "--enable-optimizations", - "--with-lto", - "--enable-static" + "--enable-shared", + "--with-system-expat", + "--with-system-ffi", + "--enable-ipv6", + "--enable-loadable-sqlite-extensions", + "--with-computed-gotos", + "--with-ensurepip=install" ] }, "phpy": { diff --git a/tools/src/Phpy/Application.php b/tools/src/Phpy/Application.php index 795cf9e..5c8f876 100644 --- a/tools/src/Phpy/Application.php +++ b/tools/src/Phpy/Application.php @@ -91,7 +91,7 @@ public static function setConfigFile(string $dir, array $data): bool|int if (file_exists($filePath)) { return false; } - return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE)); + return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); } /** @@ -116,10 +116,10 @@ public static function getLockFile(string $dir): ?string public static function setLockFile(string $dir, array $data): bool|int { $filePath = "$dir/phpy.lock"; - if (file_exists($filePath) or !isset($data['hash'])) { + if (file_exists($filePath) or !isset($data['phpy-hash']) or !isset($data['composer-hash'])) { return false; } - return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE)); + return file_put_contents($filePath, json_encode($data, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); } /** diff --git a/tools/src/Phpy/Commands/InstallCommand.php b/tools/src/Phpy/Commands/InstallCommand.php index 23e8e01..3bfcfb5 100644 --- a/tools/src/Phpy/Commands/InstallCommand.php +++ b/tools/src/Phpy/Commands/InstallCommand.php @@ -26,6 +26,7 @@ protected function configure(): void $this ->setName('install') ->setDescription('Installs the project dependencies from the phpy.lock file if present, or falls back on the phpy.json') + ->addOption('skip-build-tools', null, null, 'Skip the build tools installation.') ->addOption('skip-env', null, null, 'Skip the environment installation.') ->addOption('skip-ext', null, null, 'Skip the phpy extension installation.') ->addOption('skip-module', null, null, 'Skip the module installation.') @@ -46,6 +47,8 @@ protected function configure(): void environment specified in phpy.json to execute pip install. Use --skip-module to skip this step. +Use --skip-build-tools to skip the build tools installation. + PHPy introduces modules through Python-pip, read more at https://pypi.org/help/ EOT diff --git a/tools/src/Phpy/Config.php b/tools/src/Phpy/Config.php index d707ed9..160b7ae 100644 --- a/tools/src/Phpy/Config.php +++ b/tools/src/Phpy/Config.php @@ -21,11 +21,15 @@ class Config 'python' => [ 'source-url' => 'https://github.com/python/cpython.git', 'install-dir' => '/usr', - 'install-version' => 'latest', + 'install-version' => 'v3.13.2', 'install-configure' => [ - '--enable-optimizations', - '--with-lto', - '--enable-static' + '--enable-shared', + '--with-system-expat', + '--with-system-ffi', + '--enable-ipv6', + '--enable-loadable-sqlite-extensions', + '--with-computed-gotos', + '--with-ensurepip=install', ], ], 'phpy' => [ @@ -120,6 +124,7 @@ public function set(string $key, mixed $value): void */ public function all(): array { + $this->config['modules'] = $this->config['modules'] ?: new \stdClass(); return $this->config; } } diff --git a/tools/src/Phpy/Helpers/System.php b/tools/src/Phpy/Helpers/System.php index e359be4..3f374f1 100644 --- a/tools/src/Phpy/Helpers/System.php +++ b/tools/src/Phpy/Helpers/System.php @@ -45,7 +45,7 @@ public static function setcwd(string $cwd) public static function python(?string $path = null): string { if (file_exists($command = System::getcwd() . '/python.command')) { - return System::getFileContent($command); + return trim(System::getFileContent($command)); } if (!$path or !file_exists($path)) { if (!$path = exec('command -v python')) { @@ -65,7 +65,7 @@ public static function python(?string $path = null): string public static function pip(?string $path = null): string { if (file_exists($command = System::getcwd() . '/pip.command')) { - return System::getFileContent($command); + return trim(System::getFileContent($command)); } if (!$path or !file_exists($path)) { if (!$path = exec('command -v pip')) { @@ -85,7 +85,7 @@ public static function pip(?string $path = null): string public static function pythonConfig(?string $path = null): string { if (file_exists($command = System::getcwd() . '/python-config.command')) { - return System::getFileContent($command); + return trim(System::getFileContent($command)); } if (!$path or !file_exists($path)) { if (!$path = exec('command -v python-config')) { @@ -167,10 +167,79 @@ public static function getPackageManager(): string public static function getBuildToolsList(): array { return match (static::getPackageManager()) { - 'yum', 'zypper' => ['gcc', 'gcc-c++', 'make', 'autoconf'], - 'apk', 'apt-get' => ['gcc', 'g++', 'make', 'autoconf'], + 'apk' => [ + 'gcc', 'g++', 'make', 'autoconf', + 'musl-dev', + 'expat-dev', + 'libffi-dev', + 'openssl-dev', + 'zlib-dev', + 'xz-dev', + 'bzip2-dev', + 'sqlite-dev', + 'gdbm-dev', + 'gmp-dev', + 'pcre-dev', + 'icu-dev' + ], + 'apt-get' => [ + 'build-essential', + 'libexpat1-dev', + 'libffi-dev', + 'libssl-dev', + 'zlib1g-dev', + 'libbz2-dev', + 'libsqlite3-dev', + 'libgdbm-dev', + 'liblzma-dev', + 'libgmp-dev', + 'linux-headers-generic' + ], + 'yum' => [ + 'gcc', + 'gcc-c++', + 'make', + 'autoconf', + 'expat-devel', + 'libffi-devel', + 'openssl-devel', + 'zlib-devel', + 'bzip2-devel', + 'sqlite-devel', + 'gdbm-devel', + 'gmp-devel', + 'kernel-headers' + ], + 'zypper' => [ + 'gcc', + 'gcc-c++', + 'make', + 'autoconf', + 'libexpat-devel', + 'libffi-devel', + 'libopenssl-devel', + 'zlib-devel', + 'bzip2-devel', + 'sqlite3-devel', + 'gdbm-devel', + 'gmp-devel', + 'kernel-default-devel' + ], + 'pacman' => [ + 'gcc', + 'make', + 'autoconf', + 'expat', + 'libffi', + 'openssl', + 'zlib', + 'bzip2', + 'sqlite', + 'gdbm', + 'gmp', + 'linux-headers' + ], 'brew' => ['autoconf'], - 'pacman' => ['gcc', 'make', 'autoconf'], 'winget' => ['make', 'autoconf'], default => [], }; @@ -200,14 +269,18 @@ public static function getBuildToolsUninstall(): ?string /** * 获取系统编译依赖安装命令 * + * @param bool $check * @return string|null */ - public static function getBuildToolsInstall(): ?string + public static function getBuildToolsInstall(bool $check = true): ?string { - self::$existingPackages = self::checkExistingPackages($packages = static::getBuildToolsList()); + $packages = static::getBuildToolsList(); + self::$existingPackages = $check ? self::checkExistingPackages($packages) : []; $installPackages = array_diff($packages, self::$existingPackages); - + if (!$installPackages) { + return null; + } return match (self::getPackageManager()) { 'apk' => 'apk add --no-cache ' . implode(' ', $installPackages), 'yum' => 'sudo yum install ' . implode(' ', $installPackages), @@ -241,7 +314,7 @@ protected static function checkExistingPackages(array $packages): array shell_exec("apt list --installed | grep ^$package/"), }; - if (!empty(trim($result))) { + if ($result and !empty(trim($result))) { $existingPackages[] = $package; } } diff --git a/tools/src/Phpy/Helpers/Version.php b/tools/src/Phpy/Helpers/Version.php new file mode 100644 index 0000000..0835d40 --- /dev/null +++ b/tools/src/Phpy/Helpers/Version.php @@ -0,0 +1,41 @@ +normalize($version); + return true; + } catch (\UnexpectedValueException) { + return false; + } + } + + /** + * @param string $version + * @return int[] + */ + public static function splitVersion(string $version): array { + $cleanVer = ltrim(strtolower($version), 'v'); + preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/', $cleanVer, $matches); + + return [ + (int)($matches[1] ?? 0), + (int)($matches[2] ?? 0), + (int)($matches[3] ?? 0) + ]; + } + +} diff --git a/tools/src/Phpy/Installers/BuildToolsInstaller.php b/tools/src/Phpy/Installers/BuildToolsInstaller.php index 9aaf04e..e745e33 100644 --- a/tools/src/Phpy/Installers/BuildToolsInstaller.php +++ b/tools/src/Phpy/Installers/BuildToolsInstaller.php @@ -39,17 +39,19 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) /** @inheritdoc */ public function install(): void { + $this->consoleIO?->output('Installing/Upgrading dependency tools ...'); // 编译依赖工具 - $command = System::getBuildToolsInstall(); - if ($this->consoleIO?->ask( - "Do you want to install dependency tools? $command [Y,n]", - true, - questionClass: ConfirmationQuestion::class - )) { - if ($this->process->execute( - "$command", subOutput: true - ) !== 0) { - throw new CommandFailedException('Error installing dependency tools.'); + if ($command = System::getBuildToolsInstall()) { + if ($this->consoleIO?->ask( + "Do you want to install dependency tools? $command [Y,n]", + true, + questionClass: ConfirmationQuestion::class + )) { + if ($this->process->execute( + "$command", subOutput: true + ) !== 0) { + throw new CommandFailedException('Error installing dependency tools.'); + } } } } @@ -57,16 +59,18 @@ public function install(): void /** @inheritdoc */ public function uninstall(): void { + $this->consoleIO?->output('Uninstalling dependency tools ...'); // 卸载编译依赖工具 - $command = System::getBuildToolsUninstall(); - if ($this->consoleIO?->ask( - "Do you want to uninstall dependency tools?> [y,N])", - false, - questionClass: ConfirmationQuestion::class - )) { - $this->process->execute( - "$command", subOutput: true - ); + if ($command = System::getBuildToolsUninstall()) { + if ($this->consoleIO?->ask( + "Do you want to uninstall dependency tools?> [y,N])", + false, + questionClass: ConfirmationQuestion::class + )) { + $this->process->execute( + "$command", subOutput: true + ); + } } } diff --git a/tools/src/Phpy/Installers/ModuleInstaller.php b/tools/src/Phpy/Installers/ModuleInstaller.php index a61bf17..6c403b2 100644 --- a/tools/src/Phpy/Installers/ModuleInstaller.php +++ b/tools/src/Phpy/Installers/ModuleInstaller.php @@ -46,10 +46,6 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) $this->config = $config; $this->consoleIO = $consoleIO; $this->process = $consoleIO?->getExtra('process') ?: new Process($consoleIO); - if (!$config->get('modules')) { - $this->skipInfo = 'Module not configured. Skip install.'; - return; - } } /** @@ -80,6 +76,7 @@ public function moduleVersions(string $module): ?array */ public function scan(): void { + $this->consoleIO?->output('Scanning for modules ...'); $dirs = $this->config->get('config.scan-dirs'); if (!$dirs) { throw new CommandStopException('Nothing to scan'); @@ -132,6 +129,7 @@ public function scan(): void /** @inheritdoc */ public function install(): void { + $this->consoleIO?->output('Installing/Updating modules ...'); $modules = $this->config->get('modules', []); $vendorModules = $this->config->get('vendor-modules', []); $phpyHash = $this->config->get('phpy-hash'); diff --git a/tools/src/Phpy/Installers/PhpyInstaller.php b/tools/src/Phpy/Installers/PhpyInstaller.php index 2ada53e..a887748 100644 --- a/tools/src/Phpy/Installers/PhpyInstaller.php +++ b/tools/src/Phpy/Installers/PhpyInstaller.php @@ -52,9 +52,10 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) public function install(): void { if ($this->skipInfo) { - $this->consoleIO?->output($this->skipInfo); + $this->consoleIO?->comment($this->skipInfo); return; } + $this->consoleIO?->output('PHPy Installing ...'); $url = $this->config->get('phpy.source-url'); $version = $this->config->get('phpy.install-version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); diff --git a/tools/src/Phpy/Installers/PythonInstaller.php b/tools/src/Phpy/Installers/PythonInstaller.php index 042a66c..94cd331 100644 --- a/tools/src/Phpy/Installers/PythonInstaller.php +++ b/tools/src/Phpy/Installers/PythonInstaller.php @@ -10,6 +10,7 @@ use PhpyTool\Phpy\Exceptions\PhpyException; use PhpyTool\Phpy\Helpers\Process; use PhpyTool\Phpy\Helpers\System; +use PhpyTool\Phpy\Helpers\Version; use Symfony\Component\Console\Question\ConfirmationQuestion; class PythonInstaller implements InstallerInterface @@ -54,7 +55,7 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) $this->skipInfo = 'Python not configured. Skip install.'; return; } - if ($pythonInstallPath = $config->get('python.install-path', '/usr/bin/python')){ + if ($pythonInstallPath = $config->get('python.install-dir', '/usr') . '/bin/python3'){ if (file_exists($pythonInstallPath)) { $this->skipInfo = "Python already installed at $pythonInstallPath."; return; @@ -65,22 +66,27 @@ public function __construct(Config $config, null|ConsoleIO $consoleIO = null) /** @inheritdoc */ public function install(): void { - $version = $this->config->get('python.install-version', 'latest'); + $this->consoleIO?->output('Python Installing ...'); + $version = $this->config->get('python.install-version', 'v3.12.2'); + if (!Version::validateVersion($version)) { + throw new PhpyException("Invalid Python version: $version"); + } $cacheDir = $this->config->get('config.cache-dir'); if (str_starts_with($cacheDir, '~')) { $cacheDir = str_replace('~', getenv('HOME'), $cacheDir); } $installDir = $this->config->get('python.install-dir'); + $cwd = System::getcwd(); if (!$this->skipInfo) { + $this->consoleIO?->output('CPython-source make install ...'); $url = $this->config->get('python.source-url'); - $versionOpt = ($version === 'latest') ? '' : "--branch $version"; // 下载源码 $sourceDir = "$cacheDir/python-$version"; if (!file_exists($sourceDir)) { $this->consoleIO?->output('CPython-source Downloading ...'); if ($this->process->execute( - "git clone --depth 1 $versionOpt $url $sourceDir", subOutput: true + "git clone --depth 1 --branch $version $url $sourceDir", subOutput: true ) !== 0) { throw new CommandFailedException('Error downloading Python.'); } @@ -100,29 +106,39 @@ public function install(): void ) { throw new CommandFailedException("Error building and installing Python-$version."); } - $python = $installDir . '/bin/python'; - $pip = $installDir . '/bin/pip'; - $cwd = System::getcwd(); // 设置环境 $this->process->execute( - "echo '$python' > $cwd/python.command && echo '$pip' > $cwd/pip.command && echo '' > $cwd/python-config.command", + "echo '$installDir/bin/python3' > $cwd/python.command && echo '$installDir/bin/pip3' > $cwd/pip.command && echo '$installDir/bin/python3-config' > $cwd/python-config.command", subOutput: true ); + } else { + $this->consoleIO?->comment($this->skipInfo); } - $cwd = System::getcwd(); // 虚拟环境 if (!file_exists($venvPath = "$cwd/py-vendor")) { + $this->consoleIO?->output('Creating virtual environment...'); // 安装虚拟 $this->process->executePython("-m venv $venvPath", subOutput: true); $this->process->execute("source $venvPath/bin/activate", subOutput: true); // 软链python-config - $pythonConfigPath = "$venvPath/bin/python-config"; - $this->process->execute("ln -s $installDir/bin/python-config $pythonConfigPath", subOutput: true); + $this->consoleIO?->output('Creating python-config link...'); + $pythonConfigPath = "$venvPath/bin/python3-config"; + $this->process->execute("ln -s $installDir/bin/python3-config $pythonConfigPath", subOutput: true); + // 设置环境 + $this->process->execute( + "echo '$venvPath/bin/python3' > $cwd/python.command && echo '$venvPath/bin/pip3' > $cwd/pip.command && echo '$pythonConfigPath' > $cwd/python-config.command", + subOutput: true + ); + $this->consoleIO?->output('Creating python-include link...'); + $this->process->executePythonConfig('--includes', $output); + preg_match('/-I(.+?)(\s|$)/', $output[0], $matches); // 软链python-include - $this->process->execute("rm -rf $venvPath/include/python", subOutput: true); - $this->process->execute("ln -s $installDir/include/python $venvPath/include", subOutput: true); - + $this->process->execute("rm -rf $venvPath/include/python*", subOutput: true); + $this->process->execute("ln -s $matches[1]/ $venvPath/include", subOutput: true); + // 升级pip + $this->consoleIO?->output('Upgrading pip...'); + $this->process->executePip('install --upgrade pip', subOutput: true); } } @@ -141,7 +157,7 @@ public function uninstall(): void } // 卸载虚拟环境 $cwd = System::getcwd(); - if (file_exists($venvPath = "$cwd/py-vendor/.venv")) { + if (file_exists($venvPath = "$cwd/py-vendor")) { $this->process->execute("rm -rf $venvPath", subOutput: true); } } From 22d26cf64f38749ff5a2dfeb3f76906228496bc3 Mon Sep 17 00:00:00 2001 From: chaz6chez Date: Wed, 26 Mar 2025 10:47:35 +0800 Subject: [PATCH 6/6] fix bugs --- tools/src/Phpy/Installers/PhpyInstaller.php | 4 +++- tools/src/Phpy/Installers/PythonInstaller.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/src/Phpy/Installers/PhpyInstaller.php b/tools/src/Phpy/Installers/PhpyInstaller.php index a887748..76f0e35 100644 --- a/tools/src/Phpy/Installers/PhpyInstaller.php +++ b/tools/src/Phpy/Installers/PhpyInstaller.php @@ -55,7 +55,7 @@ public function install(): void $this->consoleIO?->comment($this->skipInfo); return; } - $this->consoleIO?->output('PHPy Installing ...'); + $this->consoleIO?->output('PHPy Installing/Upgrading ...'); $url = $this->config->get('phpy.source-url'); $version = $this->config->get('phpy.install-version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); @@ -95,6 +95,7 @@ public function install(): void /** @inheritdoc */ public function uninstall(): void { + $this->consoleIO?->output('PHPy Uninstalling ...'); $phpIniPath = $this->config->get('phpy.ini-path'); if (file_exists($phpIniPath)) { $this->process->execute("rm $phpIniPath", subOutput: true); @@ -104,6 +105,7 @@ public function uninstall(): void /** @inheritdoc */ public function upgrade(): void { + $this->skipInfo = null; $this->install(); } diff --git a/tools/src/Phpy/Installers/PythonInstaller.php b/tools/src/Phpy/Installers/PythonInstaller.php index 94cd331..182d070 100644 --- a/tools/src/Phpy/Installers/PythonInstaller.php +++ b/tools/src/Phpy/Installers/PythonInstaller.php @@ -145,6 +145,7 @@ public function install(): void /** @inheritdoc */ public function uninstall(): void { + $this->consoleIO?->output('Python Uninstalling ...'); $version = $this->config->get('python.version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); if (str_starts_with($cacheDir, '~')) { @@ -165,6 +166,7 @@ public function uninstall(): void /** @inheritdoc */ public function upgrade(): void { + $this->consoleIO?->output('Python Upgrading ...'); $version = $this->config->get('python.version', 'latest'); $cacheDir = $this->config->get('config.cache-dir'); if (str_starts_with($cacheDir, '~')) { @@ -229,6 +231,7 @@ public function upgrade(): void /** @inheritdoc */ public function clearCache(): void { + $this->consoleIO?->output('Python Cache Clearing ...'); $cacheDir = $this->config->get('config.cache-dir'); if (str_starts_with($cacheDir, '~')) { $cacheDir = str_replace('~', getenv('HOME'), $cacheDir);