diff --git a/bin/zhamao b/bin/zhamao
index c3eb7aaf..3f3c7659 100755
--- a/bin/zhamao
+++ b/bin/zhamao
@@ -21,12 +21,6 @@ else
fi
fi
-result=$(echo "$1" | grep -E "module|build")
-
-if [ "$result" != "" ]; then
- executable="$executable -d phar.readonly=off"
-fi
-
if [ -f "$(pwd)/src/entry.php" ]; then
$executable "$(pwd)/src/entry.php" $@
elif [ -f "$(pwd)/vendor/zhamao/framework/src/entry.php" ]; then
diff --git a/bin/zhamao.bat b/bin/zhamao.bat
index 1afe4b41..f6c3dbb0 100644
--- a/bin/zhamao.bat
+++ b/bin/zhamao.bat
@@ -13,7 +13,7 @@ IF /i "%ZM_CUSTOM_PHP_PATH%" neq "" (
echo "* Using system PHP executable"
SET executable=php
)
-@REM TODO: Phar write support is missing
+
IF exist src/entry.php (
@REM Run the PHP entry point
%executable% src/entry.php %*
diff --git a/src/ZM/Command/BuildCommand.php b/src/ZM/Command/BuildCommand.php
index b3cb65f3..1834e5c2 100644
--- a/src/ZM/Command/BuildCommand.php
+++ b/src/ZM/Command/BuildCommand.php
@@ -5,12 +5,10 @@ declare(strict_types=1);
namespace ZM\Command;
use Symfony\Component\Console\Attribute\AsCommand;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
-use Symfony\Component\Console\Output\OutputInterface;
+use ZM\Store\FileSystem;
-#[AsCommand(name: 'build', description: '将项目构建一个phar包')]
+#[AsCommand(name: 'build', description: '将项目构建一个 Phar 包')]
class BuildCommand extends Command
{
use NonPharLoadModeOnly;
@@ -20,81 +18,82 @@ class BuildCommand extends Command
*/
protected function configure()
{
- $this->setHelp('此功能将会把整个项目打包为phar');
- $this->addOption('target', 'D', InputOption::VALUE_REQUIRED, 'Output Directory | 指定输出目录');
- // ...
+ $this->setHelp('此功能将会把整个项目打包为 Phar');
+ $this->addOption('target', 'D', InputOption::VALUE_REQUIRED, '指定输出文件位置');
}
- protected function execute(InputInterface $input, OutputInterface $output): int
+ protected function handle(): int
{
- /* TODO
- $this->output = $output;
- $target_dir = $input->getOption('target') ?? WORKING_DIR;
- if (mb_strpos($target_dir, '../')) {
- $target_dir = realpath($target_dir);
+ $this->ensurePharWritable();
+
+ $target = $this->input->getOption('target') ?? 'zm.phar';
+ if (FileSystem::isRelativePath($target)) {
+ $target = SOURCE_ROOT_DIR . '/' . $target;
}
- if ($target_dir === false) {
- $output->writeln(TermColor::color8(31) . zm_internal_errcode('E00039') . 'Error: No such file or directory (' . $target_dir . ')' . TermColor::RESET);
- return 1;
- }
- $output->writeln('Target: ' . $target_dir);
- if (mb_substr($target_dir, -1, 1) !== '/') {
- $target_dir .= '/';
- }
- if (ini_get('phar.readonly') == 1) {
- $output->writeln(TermColor::color8(31) . zm_internal_errcode('E00040') . 'You need to set "phar.readonly" to "Off"!');
- $output->writeln(TermColor::color8(31) . 'See: https://stackoverflow.com/questions/34667606/cant-enable-phar-writing');
- return 1;
- }
- if (!is_dir($target_dir)) {
- $output->writeln(TermColor::color8(31) . zm_internal_errcode('E00039') . "Error: No such file or directory ({$target_dir})" . TermColor::RESET);
- return 1;
- }
- $filename = 'server.phar';
- $this->build($target_dir, $filename);
- */
- $output->writeln('Not implemented.');
- return 1;
+ $this->ensureTargetWritable($target);
+ $this->comment("目标文件:{$target}");
+
+ $this->info('正在构建 Phar 包');
+
+ $this->build($target, LOAD_MODE === LOAD_MODE_VENDOR ? 'src/entry.php' : 'vendor/zhamao/framework/src/entry.php');
+
+ $this->info('Phar 包构建完成');
+
+ return self::SUCCESS;
}
- /*
- private function build($target_dir, $filename)
- {
- @unlink($target_dir . $filename);
- $phar = new Phar($target_dir . $filename);
- $phar->startBuffering();
- $all = DataProvider::scanDirFiles(DataProvider::getSourceRootDir(), true, true);
-
- $all = array_filter($all, function ($x) {
- $dirs = preg_match('/(^(bin|config|resources|src|vendor)\\/|^(composer\\.json|README\\.md)$)/', $x);
- return !($dirs !== 1);
- });
-
- sort($all);
-
- $archive_dir = DataProvider::getSourceRootDir();
- $map = [];
-
- if (class_exists('\\League\\CLImate\\CLImate')) {
- $climate = new CLImate();
- $progress = $climate->progress()->total(count($all));
+ private function ensurePharWritable(): void
+ {
+ if (ini_get('phar.readonly') === '1') {
+ if (!function_exists('pcntl_exec')) {
+ $this->error('Phar 处于只读模式,且 pcntl 扩展未加载,无法自动切换到读写模式。');
+ $this->error('请修改 php.ini 中的 phar.readonly 为 0,或执行 php -d phar.readonly=0 ' . $_SERVER['PHP_SELF'] . ' build');
+ exit(1);
}
- foreach ($all as $k => $v) {
- $map[$v] = $archive_dir . '/' . $v;
- if (isset($progress)) {
- $progress->current($k + 1, 'Adding ' . $v);
- }
+ // Windows 下无法使用 pcntl_exec
+ if (DIRECTORY_SEPARATOR === '\\') {
+ $this->error('Phar 处于只读模式,且当前运行环境为 Windows,无法自动切换到读写模式。');
+ $this->error('请修改 php.ini 中的 phar.readonly 为 0,或执行 php -d phar.readonly=0 ' . $_SERVER['PHP_SELF'] . ' build');
+ exit(1);
+ }
+ $this->info('Phar 处于只读模式,正在尝试切换到读写模式...');
+ sleep(1);
+ $args = array_merge(['php', '-d', 'phar.readonly=0'], $_SERVER['argv']);
+ if (pcntl_exec('/usr/bin/env', $args) === false) {
+ $this->error('切换到读写模式失败,请检查环境。');
+ exit(1);
}
- $this->output->write('Building...');
- $phar->buildFromIterator(new ArrayIterator($map));
- $phar->setStub(
- "#!/usr/bin/env php\n" .
- $phar->createDefaultStub(LOAD_MODE == 0 ? 'src/entry.php' : 'vendor/zhamao/framework/src/entry.php')
- );
- $phar->stopBuffering();
- $this->output->writeln('');
- $this->output->writeln('Successfully built. Location: ' . $target_dir . "{$filename}");
- $this->output->writeln('You may use `chmod +x server.phar` to let phar executable with `./` command');
}
- */
+ }
+
+ private function ensureTargetWritable(string $target): void
+ {
+ if (file_exists($target) && !is_writable($target)) {
+ $this->error('目标文件不可写:' . $target);
+ exit(1);
+ }
+ }
+
+ private function build(string $target, string $entry): void
+ {
+ $phar = new \Phar($target, 0, $target);
+
+ $phar->startBuffering();
+ $files = FileSystem::scanDirFiles(SOURCE_ROOT_DIR, true, true);
+ $files = array_filter($files, function ($x) {
+ $dirs = preg_match('/(^(bin|config|resources|src|vendor)\\/|^(composer\\.json|README\\.md)$)/', $x);
+ return !($dirs !== 1);
+ });
+ sort($files);
+
+ foreach ($this->progress()->iterate($files) as $file) {
+ $phar->addFile($file, $file);
+ }
+
+ $phar->setStub(
+ '#!/usr/bin/env php' . PHP_EOL .
+ $phar::createDefaultStub($entry)
+ );
+ $phar->stopBuffering();
+ }
}
diff --git a/src/ZM/Command/Command.php b/src/ZM/Command/Command.php
index ebb87910..93144cb8 100644
--- a/src/ZM/Command/Command.php
+++ b/src/ZM/Command/Command.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ZM\Command;
+use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
@@ -151,4 +152,21 @@ abstract class Command extends \Symfony\Component\Console\Command\Command
exit(self::FAILURE);
}
}
+
+ /**
+ * 获取一个进度条实例
+ *
+ * @param int $max 最大进度值,可以稍后再设置
+ */
+ protected function progress(int $max = 0): ProgressBar
+ {
+ $progress = new ProgressBar($this->output, $max);
+ $progress->setBarCharacter('⚬>');
+ $progress->setEmptyBarCharacter('⚬>');
+ $progress->setProgressCharacter('➤>');
+ $progress->setFormat(
+ "%current%/%max% [%bar%] %percent:3s%%\n🪅 %estimated:-20s% %memory:20s%"
+ );
+ return $progress;
+ }
}
diff --git a/zhamao b/zhamao
index c3eb7aaf..3f3c7659 100755
--- a/zhamao
+++ b/zhamao
@@ -21,12 +21,6 @@ else
fi
fi
-result=$(echo "$1" | grep -E "module|build")
-
-if [ "$result" != "" ]; then
- executable="$executable -d phar.readonly=off"
-fi
-
if [ -f "$(pwd)/src/entry.php" ]; then
$executable "$(pwd)/src/entry.php" $@
elif [ -f "$(pwd)/vendor/zhamao/framework/src/entry.php" ]; then