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
69 changes: 69 additions & 0 deletions .github/scripts/check-binary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/bin/sh
# Checks a packaged SKERNEL with an empty environment and no PHP on the PATH, then uses it to create, package, and serve
# a project.
set -eu

binary=$1
work=$(mktemp -d)
server=

stop_server() {
if [ -n "$server" ]; then
kill -TERM "$server" 2>/dev/null || true
wait "$server" 2>/dev/null || true
server=
fi
}

trap stop_server EXIT

cd "$work"

errors=$(env -i "$binary" --version 2>&1 >/dev/null)
if [ -n "$errors" ]; then
echo "SKERNEL wrote to STDERR: $errors" >&2
exit 1
fi

version=$(env -i "$binary" --version)
echo "$version"
if [ -n "${VERSION:-}" ] && [ "$version" != "SKERNEL $VERSION" ]; then
echo "Expected SKERNEL $VERSION." >&2
exit 1
fi

env -i "$binary" list --raw

"$binary" new "$work/app"
"$binary" package -d "$work/app" --output="$work/app-binary"

mkdir "$work/elsewhere"
mv "$work/app-binary" "$work/elsewhere/app"
cd "$work/elsewhere"
env -i ./app swoole:start >"$work/server.log" 2>&1 &
server=$!

attempt=0
until curl --silent --fail --max-time 2 --output /dev/null http://127.0.0.1:9600/index; do
attempt=$((attempt + 1))
if [ "$attempt" -ge 60 ] || ! kill -0 "$server" 2>/dev/null; then
cat "$work/server.log" >&2
exit 1
fi
sleep 1
done

curl --silent --fail http://127.0.0.1:9600/user
echo

kill -TERM "$server"
status=0
wait "$server" || status=$?
server=
if [ "$status" -ne 0 ]; then
cat "$work/server.log" >&2
echo "The application exited with status $status." >&2
exit 1
fi

echo "Checked $version."
118 changes: 118 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Release

# A tag that is a SemVer 2.0 version publishes a release. A pull request that changes the release process builds and
# checks every binary without publishing anything.
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+*'
pull_request:
paths:
- .github/scripts/**
- .github/workflows/release.yml

permissions:
contents: read

jobs:
version:
name: Check the tag
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Require a SemVer 2.0 version
id: version
env:
TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
semver='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$'

if [ -n "$TAG" ] && ! printf '%s' "$TAG" | grep -Eq "$semver"; then
echo "::error::The tag $TAG is not a SemVer 2.0 version."
exit 1
fi

echo "version=$TAG" >> "$GITHUB_OUTPUT"

build:
name: Build ${{ matrix.platform }}
needs: version
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x86_64
runner: ubuntu-24.04
- platform: linux-aarch64
runner: ubuntu-24.04-arm
- platform: macos-aarch64
runner: macos-15
- platform: macos-x86_64
runner: macos-15-intel
env:
# static-php-cli looks up its sources through the GitHub API, whose anonymous limit runners share.
GITHUB_TOKEN: ${{ github.token }}
VERSION: ${{ needs.version.outputs.version }}
BINARY: dist/skernel-${{ matrix.platform }}
steps:
- uses: actions/checkout@v7

- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: iconv, openssl, pcntl, phar, tokenizer, zlib
tools: composer:v2

- name: Install the build tools on Linux
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y autoconf automake autopoint bison cmake flex gettext libtool re2c

- name: Install the build tools on macOS
if: runner.os == 'macOS'
run: |
brew install autoconf automake bison cmake gettext libtool re2c
echo "$(brew --prefix bison)/bin" >> "$GITHUB_PATH"

- name: Set the version of the binary
if: env.VERSION != ''
run: composer config version "$VERSION"

- name: Install the dependencies
run: composer install --no-dev --no-interaction --no-progress

- name: Package SKERNEL
run: php bin/bootstrap.php package --output="$BINARY"

- name: Check the binary
run: .github/scripts/check-binary.sh "$PWD/$BINARY"

- uses: actions/upload-artifact@v7
with:
name: skernel-${{ matrix.platform }}
path: ${{ env.BINARY }}
if-no-files-found: error

release:
name: Publish the release
needs:
- version
- build
if: github.ref_type == 'tag'
runs-on: ubuntu-24.04
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true

# SKERNEL keeps no older version line, so the newest tag is always the latest release, which self-update installs.
- name: Publish the binaries
run: gh release create "$VERSION" dist/* --verify-tag --title "$VERSION" --generate-notes --latest
11 changes: 8 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 @@ -153,11 +153,16 @@ Package written to /path/to/my-app/target/my-app.

A missing `composer.lock`, a runtime file that is not found, a failed installation, build, or archive, or any of the failures of `build` prints a short message on STDERR and exits with status 1.

## Releases

Pushing a tag that is a [SemVer 2.0](https://semver.org) version, such as `1.2.3` or `1.3.0-rc.1`, without a `v` prefix, publishes a [GitHub Release](https://github.com/super-kernel/skernel/releases) with a binary for each of `linux-x86_64`, `linux-aarch64`, `macos-aarch64`, and `macos-x86_64`. Each binary is checked by creating, packaging, and serving a project before anything is published. Any other tag fails.

The newest tag is always the [latest release](https://github.com/super-kernel/skernel/releases/latest), which `self-update` installs, whatever its version says. SKERNEL keeps no older version line and does not support moving back to an older version.

## Requirements

- 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
11 changes: 8 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 @@ -153,11 +153,16 @@ Package written to /path/to/my-app/target/my-app.

缺少 `composer.lock`、找不到运行时文件、安装、构建或归档失败,或出现 `build` 的任一种失败时,会在 STDERR 上打印一条简短的提示,退出码为 1。

## 发布

推送一个符合 [SemVer 2.0](https://semver.org) 的标签(例如 `1.2.3` 或 `1.3.0-rc.1`,不带 `v` 前缀)就会发布一个 [GitHub Release](https://github.com/super-kernel/skernel/releases),其中包含 `linux-x86_64`、`linux-aarch64`、`macos-aarch64` 和 `macos-x86_64` 各一个二进制。发布之前,每个二进制都要通过创建、打包并运行一个项目的检查。其他标签会失败。

最新的标签始终是[最新发布](https://github.com/super-kernel/skernel/releases/latest),`self-update` 安装的就是它,无论版本号如何。SKERNEL 不维护旧版本线,也不支持退回旧版本。

## 环境要求

- 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
Loading
Loading