diff --git a/README.md b/README.md index b1abfc4..b91d000 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Running SKERNEL without a command, or with `list`, prints the SKERNEL logo above the plain-text overview of the available commands. The raw, JSON, XML, and Markdown listings, namespace listings, and command help omit the logo. `--version` prints `SKERNEL`, followed by the version when the package declares one. -SKERNEL writes log records of WARNING and above, including the framework's scan summaries and exception reports, to STDERR, so STDOUT carries only command output; for example, `list --format=json` writes plain JSON to STDOUT. +SKERNEL writes log records of WARNING and above, including the framework's exception reports, to STDERR, so STDOUT carries only command output; for example, `list --format=json` writes plain JSON to STDOUT. A usage error, such as an unknown command or option, prints a short message on STDERR, followed by the running command's synopsis when a command's input is invalid, and exits with status 1. Add `-v` to also print the exception class and trace. @@ -140,7 +140,7 @@ package [-d|--working-dir=] [--php=] [--phar] [--dev] [--output=

=8.4.27 <8.5` with the `openssl`, `pcntl`, `phar`, and `zlib` extensions; Composer checks the required PHP extensions during installation. - Network access and static-php-cli's build tools the first time a managed PHP is built, as described under [Managed PHP](#managed-php). -- Composer for `package`. ## Running from source diff --git a/README.zh-CN.md b/README.zh-CN.md index 9fc6968..d1e841b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -13,7 +13,7 @@ 不带命令运行 SKERNEL,或运行 `list` 时,纯文本的命令总览上方会打印 SKERNEL 标志。raw、JSON、XML 和 Markdown 格式的列表、按命名空间列出的列表以及命令帮助都不打印标志。`--version` 输出 `SKERNEL`,包声明了版本时后面跟上版本号。 -SKERNEL 把 WARNING 及以上级别的日志(包括框架的扫描汇总和异常报告)写到 STDERR,STDOUT 只承载命令本身的输出;例如 `list --format=json` 在 STDOUT 上输出的就是纯 JSON。 +SKERNEL 把 WARNING 及以上级别的日志(包括框架的异常报告)写到 STDERR,STDOUT 只承载命令本身的输出;例如 `list --format=json` 在 STDOUT 上输出的就是纯 JSON。 命令不存在、选项不存在这类用法错误,会在 STDERR 上打印一条简短的提示;如果是某个命令的输入有误,后面还会打印该命令的用法。退出码为 1。加上 `-v` 时还会显示异常类名和堆栈。 @@ -140,7 +140,7 @@ package [-d|--working-dir=

] [--php=] [--phar] [--dev] [--output=

=8.4.27 <8.5`,并带有 `openssl`、`pcntl`、`phar` 和 `zlib` 扩展;安装时由 Composer 检查所需的 PHP 扩展。 - 首次构建受管 PHP 时需要联网,以及 static-php-cli 的构建工具,详见[受管 PHP](#受管-php)。 -- `package` 需要 Composer。 ## 从源码运行 diff --git a/skernel.schema.json b/skernel.schema.json index 9d73f73..692769b 100644 --- a/skernel.schema.json +++ b/skernel.schema.json @@ -36,7 +36,7 @@ "minLength": 1 }, "composer": { - "description": "The Composer binary that installs the packaged copy, a path or a name looked up in PATH.", + "description": "The Composer binary that installs the packaged copy, a path or a name looked up in PATH, by default a working Composer 2 or else SKERNEL's own.", "type": "string", "minLength": 1 }, diff --git a/src/Command/PackageCommand.php b/src/Command/PackageCommand.php index 7ea8482..5c80156 100644 --- a/src/Command/PackageCommand.php +++ b/src/Command/PackageCommand.php @@ -5,6 +5,7 @@ namespace Src\Command; use Src\Build\ProjectBuilder; +use Src\Composer\ComposerLocator; use Src\Configuration\ConfigurationLoader; use Src\Filesystem\Directory; use Src\Package\DependencyInstaller; @@ -44,6 +45,7 @@ final class PackageCommand extends Command public function __construct( private readonly ProjectLocator $projectLocator, private readonly ConfigurationLoader $configurationLoader, + private readonly ComposerLocator $composerLocator, private readonly DependencyInstaller $dependencyInstaller, private readonly ManagedPhp $managedPhp, private readonly ProjectBuilder $projectBuilder, @@ -68,7 +70,7 @@ protected function configure(): void 'composer', null, InputOption::VALUE_REQUIRED, - 'The Composer binary that installs the packaged copy, by default composer', + "The Composer binary that installs the packaged copy, by default a working Composer 2 or else SKERNEL's own", ); $this->addOption( 'runtime', @@ -130,7 +132,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]); $status = $this->dependencyInstaller->install( $staging . '/project', - $input->getOption('composer') ?? $configuration->composer ?? 'composer', + $this->composerLocator->command( + $input->getOption('composer') ?? $configuration->composer, + $errorOutput, + ), $input->getOption('dev') || true === $configuration->dev, $errorOutput, ); diff --git a/src/Logger/StderrLogger.php b/src/Logger/StderrLogger.php index 49a5880..a5cd2b7 100644 --- a/src/Logger/StderrLogger.php +++ b/src/Logger/StderrLogger.php @@ -13,14 +13,22 @@ use SuperKernel\Logger\Factory\LoggerFactory; use SuperKernel\Logger\Formatter\ConsoleFormatter; +use function is_string; +use function str_starts_with; + /** * Writes SKERNEL and framework log records of WARNING and above to STDERR, keeping STDOUT for command output. + * + * The framework's report of the classes that a scan of SKERNEL itself skipped is left out, because it says nothing that + * a SKERNEL user can act on. A scanned project still reports them through its own logger. */ #[Provider(StdoutLoggerInterface::class)] final readonly class StderrLogger implements StdoutLoggerInterface { use LoggerTrait; + private const string SCAN_DIAGNOSTIC_EVENT = 'annotation_scan.'; + private LoggerInterface $logger; public function __construct(LoggerFactory $factory) @@ -35,6 +43,12 @@ public function __construct(LoggerFactory $factory) public function log(mixed $level, string|Stringable $message, array $context = []): void { + $event = $context['event'] ?? null; + + if (is_string($event) && str_starts_with($event, self::SCAN_DIAGNOSTIC_EVENT)) { + return; + } + $this->logger->log($level, $message, $context); } } diff --git a/src/Package/DependencyInstaller.php b/src/Package/DependencyInstaller.php index 262a96f..b09578a 100644 --- a/src/Package/DependencyInstaller.php +++ b/src/Package/DependencyInstaller.php @@ -4,7 +4,6 @@ namespace Src\Package; -use Src\Process\Executable; use Src\Process\ProcessRunner; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Output\OutputInterface; @@ -20,17 +19,17 @@ final readonly class DependencyInstaller { /** - * @param string $composerBinary A path or a name looked up in PATH - * @param bool $dev Whether development dependencies are installed too + * @param non-empty-list $composer The command that runs Composer + * @param bool $dev Whether development dependencies are installed too * * @return int The exit status of Composer * - * @throws RuntimeException When Composer is not found or cannot be started + * @throws RuntimeException When Composer cannot be started */ - public function install(string $directory, string $composerBinary, bool $dev, OutputInterface $output): int + public function install(string $directory, array $composer, bool $dev, OutputInterface $output): int { $command = [ - Executable::find($composerBinary, 'Composer'), + ...$composer, 'install', '--no-interaction', '--no-progress', @@ -41,7 +40,7 @@ public function install(string $directory, string $composerBinary, bool $dev, Ou $command[] = '--no-dev'; } - return ProcessRunner::run($command, $directory, $output, sprintf('the Composer binary %s', $composerBinary), [ + return ProcessRunner::run($command, $directory, $output, sprintf('the Composer binary %s', $composer[0]), [ 'COMPOSER_MIRROR_PATH_REPOS' => '1', ]); } diff --git a/tests/BootstrapTest.php b/tests/BootstrapTest.php index 293c3b6..f9c500f 100644 --- a/tests/BootstrapTest.php +++ b/tests/BootstrapTest.php @@ -85,6 +85,14 @@ public function testStandardOutputCarriesOnlyTheCommandOutput(): void self::assertSame('SKERNEL', $listing['application']['name']); } + public function testScanDiagnosticsOfSKERNELItselfAreNotWritten(): void + { + $output = $this->bootstrap('--version', $status, '2>&1 >/dev/null'); + + self::assertSame(0, $status, implode("\n", $output)); + self::assertSame([], $output); + } + public function testLogRecordsBelowWarningAreNotWritten(): void { $output = $this->bootstrap('list --format=json', $status, '2>&1 >/dev/null'); diff --git a/tests/Integration/PackageCommandTest.php b/tests/Integration/PackageCommandTest.php index cb546e7..764acdf 100644 --- a/tests/Integration/PackageCommandTest.php +++ b/tests/Integration/PackageCommandTest.php @@ -111,6 +111,7 @@ protected function setUp(): void $this->write($this->project . '/target/stale', 'stale'); $this->write($this->directory . '/bin/composer', implode("\n", [ '#!/bin/sh', + 'if [ "$1" = --version ]; then echo "Composer version 2.10.3"; exit 0; fi', 'printf "%s" "$*" > ' . escapeshellarg($this->directory . '/composer-arguments'), 'cp -R ' . escapeshellarg($this->directory . '/template/vendor') . ' vendor', '', @@ -222,6 +223,25 @@ public function testPackagesAProjectWhoseDependenciesAreNotInstalled(): void self::assertDirectoryDoesNotExist($this->project . '/vendor'); } + public function testFindsAWorkingComposerWhenNoneIsGiven(): void + { + exec( + 'PATH=' . escapeshellarg($this->directory . '/bin:/usr/bin:/bin') + . ' HOME=' . escapeshellarg($this->directory) + . ' SKERNEL_HOME=' . escapeshellarg($this->directory . '/home') . ' ' + . escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(dirname(__DIR__, 2) . '/bin/bootstrap.php') + . ' package --phar --php=' . $this->version . ' -d ' . escapeshellarg($this->project) . ' 2>/dev/null', + $output, + $status, + ); + + self::assertSame(0, $status, implode("\n", $output)); + self::assertStringStartsWith( + 'install --no-interaction', + (string) file_get_contents($this->directory . '/composer-arguments'), + ); + } + public function testRejectsAProjectWithoutALockFile(): void { unlink($this->project . '/composer.lock');