Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -140,7 +140,7 @@ package [-d|--working-dir=<dir>] [--php=<version>] [--phar] [--dev] [--output=<p
`package` turns the project into a standalone executable binary, or into a PHAR with `--phar`. It works on a temporary copy of the project, so the project itself is not changed:

1. It copies the project without its vendor directory, VCS metadata such as `.git`, its `target/` directory, and the `.env` and `.env.*` files in its root directory, which usually hold machine-local secrets; a packaged application reads its environment from the process environment.
2. It installs the copy's dependencies from the project's `composer.lock` with the Composer binary given with `--composer`, a path or a name looked up in `PATH`, by default `composer`. Development dependencies are left out unless `--dev` is given. The project must have a `composer.lock`, but its own dependencies need not be installed. Composer does not check platform requirements against the PHP that runs it, because the managed PHP is the package's platform.
2. It installs the copy's dependencies from the project's `composer.lock` with the Composer that [`new`](#new) would use: the binary given with `--composer`, a path or a name looked up in `PATH`, or else the first working Composer 2 it finds, or else its own. Development dependencies are left out unless `--dev` is given. The project must have a `composer.lock`, but its own dependencies need not be installed. Composer does not check platform requirements against the PHP that runs it, because the managed PHP is the package's platform.
3. It runs `build` on the copy with the managed PHP of that installation.
4. It archives the copy into a PHAR whose stub loads the copy's autoloader, obtains the application container, and runs the application, so no entry script is needed.
5. It writes the managed micro runtime, or the file given with `--runtime`, followed by the PHAR to the output path, or only the PHAR with `--phar`, and makes the result executable. A micro runtime given with `--runtime` must report `cli` as `PHP_SAPI` for the result to start a Swoole server; with static-php-cli, build it with `--with-micro-fake-cli`.
Expand All @@ -157,7 +157,6 @@ A missing `composer.lock`, a runtime file that is not found, a failed installati

- PHP `>=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

Expand Down
5 changes: 2 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 时还会显示异常类名和堆栈。

Expand Down Expand Up @@ -140,7 +140,7 @@ package [-d|--working-dir=<dir>] [--php=<version>] [--phar] [--dev] [--output=<p
`package` 把项目做成可直接执行的独立二进制,指定 `--phar` 时则做成 PHAR。它在项目的临时副本上工作,不会改动项目本身:

1. 复制项目,但不包括 vendor 目录、`.git` 等 VCS 元数据、`target/` 目录,以及根目录下的 `.env` 和 `.env.*` 文件(这些文件通常保存本机密钥);打包后的应用从进程环境变量读取环境配置。
2. 按项目的 `composer.lock` 为副本安装依赖,使用的 Composer 由 `--composer` 指定,可以是路径,也可以是在 `PATH` 中查找的名称,默认是 `composer`。除非指定 `--dev`,否则不安装开发依赖。项目必须有 `composer.lock`,但项目自身不必安装依赖。Composer 不会按运行它的那个 PHP 检查平台要求,因为包的运行平台是受管 PHP。
2. 按项目的 `composer.lock` 为副本安装依赖,使用的 Composer 与 [`new`](#new) 相同:`--composer` 指定的程序(路径或在 `PATH` 中查找的名称),否则是找到的第一个可用的 Composer 2,再否则是 SKERNEL 自己的。除非指定 `--dev`,否则不安装开发依赖。项目必须有 `composer.lock`,但项目自身不必安装依赖。Composer 不会按运行它的那个 PHP 检查平台要求,因为包的运行平台是受管 PHP。
3. 用该次安装对应的受管 PHP 对副本运行 `build`。
4. 把副本归档为 PHAR,它的 stub 加载副本的 autoloader、获取应用容器并运行应用,因此不需要入口脚本。
5. 把受管 micro 运行时(或 `--runtime` 指定的文件)和 PHAR 依次写入输出路径;指定 `--phar` 时只写入 PHAR。结果文件会被设为可执行。用 `--runtime` 指定的 micro 运行时必须把 `PHP_SAPI` 报告为 `cli`,结果文件才能启动 Swoole 服务;使用 static-php-cli 时,构建时加上 `--with-micro-fake-cli`。
Expand All @@ -157,7 +157,6 @@ Package written to /path/to/my-app/target/my-app.

- PHP `>=8.4.27 <8.5`,并带有 `openssl`、`pcntl`、`phar` 和 `zlib` 扩展;安装时由 Composer 检查所需的 PHP 扩展。
- 首次构建受管 PHP 时需要联网,以及 static-php-cli 的构建工具,详见[受管 PHP](#受管-php)。
- `package` 需要 Composer。

## 从源码运行

Expand Down
2 changes: 1 addition & 1 deletion skernel.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
9 changes: 7 additions & 2 deletions src/Command/PackageCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -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,
);
Expand Down
14 changes: 14 additions & 0 deletions src/Logger/StderrLogger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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);
}
}
13 changes: 6 additions & 7 deletions src/Package/DependencyInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string> $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',
Expand All @@ -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',
]);
}
Expand Down
8 changes: 8 additions & 0 deletions tests/BootstrapTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
20 changes: 20 additions & 0 deletions tests/Integration/PackageCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
'',
Expand Down Expand Up @@ -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');
Expand Down