diff --git a/.github/scripts/check-binary.sh b/.github/scripts/check-binary.sh new file mode 100755 index 0000000..b225698 --- /dev/null +++ b/.github/scripts/check-binary.sh @@ -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." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..45a9cd2 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.md b/README.md index b1abfc4..02de034 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=
=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..1612507 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=
=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