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
2 changes: 1 addition & 1 deletion docs/binaries-phar-docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The PHAR builder copies only runtime inputs into the build stage: `config/`, `pu

PHPDoc comments are stripped from packaged PHP files to keep the standalone PHAR and embedded static-binary PHAR smaller while preserving runtime comments, code, and dependency PHPDoc that is read through reflection at runtime. Benchmark fixture helpers, Composer's `installed.json`, VCS placeholders, and non-runtime type stubs are omitted because packaged commands do not need them.
Packaged PHAR entries are gzip-compressed before the archive is finalized. The static binary appends that same PHAR to the micro SAPI executable, so PHAR compression reduces both the standalone PHAR and the Linux, macOS, and Windows static executables.
The static executable includes `ext-highlighter`, so syntax highlighting does not need FFI or an external shared library. `serve` uses ReactPHP stream sockets with preforked worker processes, serves built files and live reload SSE in the server loop, keeps one shared live reload watcher per worker, and does not require PHP's native `sockets` extension.
The static executable includes `ext-highlighter`, so syntax highlighting does not need FFI or an external shared library. `serve` uses ReactPHP stream sockets, serves built files and live reload SSE in the server loop, keeps one shared live reload watcher per server process, and does not require PHP's native `sockets` extension. On POSIX platforms with PCNTL and signal support, `serve` can prefork worker processes; Windows static binaries run a single server process.
Relative `content-dir`, `output-dir`, `new`, `clean`, `serve`, and `import` paths are resolved from the directory where you run `yiipress`, not from the packaged executable location.
PHAR and static binary runs keep build cache and incremental manifests under the OS temp directory, keyed by the current project directory, instead of writing to `runtime/` in the site checkout. `yiipress clean` removes that packaged cache as well as the configured output directory.

Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Starts the preview server for local development.
- `--content-dir`, `-c` — path to the content directory (default: `content`). Absolute or relative to project root.
- `--output-dir`, `-o` — path to the output directory served by the preview server (default: `output`). Absolute or relative to project root.
- `--port`, `-p` — port to serve at when the address argument does not include a port (default: `19777`).
- `--workers`, `-w` — number of preforked server workers (default: `2`).
- `--workers`, `-w` — number of preforked server workers on POSIX platforms with PCNTL support (default: `2`). Windows and other runtimes without signal support run a single server process.

On startup, `serve` prints the URL it is listening on. Build progress is printed by rebuilds triggered after file changes. Content and output paths resolve from the current working directory, so run the binary from the site directory or pass explicit `--content-dir` and `--output-dir` paths.

Expand Down
2 changes: 2 additions & 0 deletions docs/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The preview server serves the built site during local development:

The static binary is the recommended way to run preview. It resolves `content/` and `output/` from the current directory by default, and custom paths can be passed with `--content-dir` and `--output-dir`.

On Windows, the preview server runs as a single ReactPHP server process. Preforked `--workers` and POSIX signal handlers are used only on platforms where PCNTL and signal constants are available.

The command validates paths before opening the socket. The content directory must exist, and the output directory must exist or be creatable and writable. If either check fails, run it with explicit paths such as `./yiipress serve --content-dir=content --output-dir=output`.

## Static file serving
Expand Down
1 change: 1 addition & 0 deletions roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
## Priority 3: Developer experience

- [x] Live reload / file watching during `yiipress serve`
- [x] Windows-compatible `yiipress serve` without PCNTL or POSIX signal handlers
- [x] PHAR and static PHP binary packaging
- [x] Worker-mode serving for PHAR and static PHP binary packages
- [x] Linux, macOS, Windows, and distroless Docker release packaging
Expand Down
14 changes: 11 additions & 3 deletions src/Console/ServeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ final class ServeCommand extends Command
private int $nextLiveReloadClientId = 1;
private ?TimerInterface $liveReloadPingTimer = null;

public function __construct(
private readonly ServeRuntimeCapabilities $runtimeCapabilities = new ServeRuntimeCapabilities(),
) {
parent::__construct();
}

public function configure(): void
{
$this
Expand Down Expand Up @@ -195,7 +201,7 @@ private function executeReactServer(InputInterface $input, OutputInterface $outp
return ExitCode::UNSPECIFIED_ERROR;
}

if ($workers > 1 && function_exists('pcntl_fork')) {
if ($workers > 1 && $this->runtimeCapabilities->supportsWorkerPool()) {
return $this->runWorkerPool($server, $address, $workers);
}

Expand Down Expand Up @@ -284,8 +290,10 @@ private function runServer(SocketServer $server, string $address): int
$server->close();
$loop->stop();
};
$loop->addSignal(\SIGINT, $stop);
$loop->addSignal(\SIGTERM, $stop);
if ($this->runtimeCapabilities->supportsEventLoopSignals()) {
$loop->addSignal(\SIGINT, $stop);
$loop->addSignal(\SIGTERM, $stop);
}
$loop->run();

return ExitCode::OK;
Expand Down
72 changes: 72 additions & 0 deletions src/Console/ServeRuntimeCapabilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace YiiPress\Console;

use function defined;
use function function_exists;

final readonly class ServeRuntimeCapabilities
{
private string $directorySeparator;
private bool $sigintDefined;
private bool $sigtermDefined;
private bool $pcntlAsyncSignalsAvailable;
private bool $pcntlForkAvailable;
private bool $pcntlSignalDispatchAvailable;
private bool $pcntlSignalAvailable;
private bool $pcntlWaitAvailable;
private bool $pcntlWaitStatusAvailable;
private bool $posixKillAvailable;

public function __construct(
?string $directorySeparator = null,
?bool $sigintDefined = null,
?bool $sigtermDefined = null,
?bool $pcntlAsyncSignalsAvailable = null,
?bool $pcntlForkAvailable = null,
?bool $pcntlSignalDispatchAvailable = null,
?bool $pcntlSignalAvailable = null,
?bool $pcntlWaitAvailable = null,
?bool $pcntlWaitStatusAvailable = null,
?bool $posixKillAvailable = null,
) {
$this->directorySeparator = $directorySeparator ?? \DIRECTORY_SEPARATOR;
$this->sigintDefined = $sigintDefined ?? defined('SIGINT');
$this->sigtermDefined = $sigtermDefined ?? defined('SIGTERM');
$this->pcntlAsyncSignalsAvailable = $pcntlAsyncSignalsAvailable ?? function_exists('pcntl_async_signals');
$this->pcntlForkAvailable = $pcntlForkAvailable ?? function_exists('pcntl_fork');
$this->pcntlSignalDispatchAvailable = $pcntlSignalDispatchAvailable
?? function_exists('pcntl_signal_dispatch');
$this->pcntlSignalAvailable = $pcntlSignalAvailable ?? function_exists('pcntl_signal');
$this->pcntlWaitAvailable = $pcntlWaitAvailable ?? function_exists('pcntl_wait');
$this->pcntlWaitStatusAvailable = $pcntlWaitStatusAvailable
?? (
function_exists('pcntl_wexitstatus')
&& function_exists('pcntl_wifexited')
&& function_exists('pcntl_wifsignaled')
);
$this->posixKillAvailable = $posixKillAvailable ?? function_exists('posix_kill');
}

public function supportsEventLoopSignals(): bool
{
return $this->sigintDefined
&& $this->sigtermDefined
&& $this->pcntlSignalAvailable
&& $this->pcntlSignalDispatchAvailable;
}

public function supportsWorkerPool(): bool
{
return $this->directorySeparator !== '\\'
&& $this->supportsEventLoopSignals()
&& $this->pcntlAsyncSignalsAvailable
&& $this->pcntlForkAvailable
&& $this->pcntlSignalAvailable
&& $this->pcntlWaitAvailable
&& $this->pcntlWaitStatusAvailable
&& $this->posixKillAvailable;
}
}
112 changes: 112 additions & 0 deletions tests/Unit/Console/ServeRuntimeCapabilitiesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace YiiPress\Tests\Unit\Console;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use YiiPress\Console\ServeRuntimeCapabilities;

final class ServeRuntimeCapabilitiesTest extends TestCase
{
#[Test]
public function windowsRuntimeDoesNotSupportWorkerPoolOrEventLoopSignals(): void
{
$capabilities = new ServeRuntimeCapabilities(
directorySeparator: '\\',
sigintDefined: false,
sigtermDefined: false,
pcntlAsyncSignalsAvailable: true,
pcntlForkAvailable: true,
pcntlSignalDispatchAvailable: true,
pcntlSignalAvailable: true,
pcntlWaitAvailable: true,
pcntlWaitStatusAvailable: true,
posixKillAvailable: true,
);

self::assertFalse($capabilities->supportsWorkerPool());
self::assertFalse($capabilities->supportsEventLoopSignals());
}

#[Test]
public function missingSignalConstantsDisableServeSignalHooks(): void
{
$capabilities = new ServeRuntimeCapabilities(
directorySeparator: '/',
sigintDefined: false,
sigtermDefined: true,
pcntlAsyncSignalsAvailable: true,
pcntlForkAvailable: true,
pcntlSignalDispatchAvailable: true,
pcntlSignalAvailable: true,
pcntlWaitAvailable: true,
pcntlWaitStatusAvailable: true,
posixKillAvailable: true,
);

self::assertFalse($capabilities->supportsWorkerPool());
self::assertFalse($capabilities->supportsEventLoopSignals());
}

#[Test]
public function unixRuntimeWithPcntlAndSignalsSupportsWorkerPool(): void
{
$capabilities = new ServeRuntimeCapabilities(
directorySeparator: '/',
sigintDefined: true,
sigtermDefined: true,
pcntlAsyncSignalsAvailable: true,
pcntlForkAvailable: true,
pcntlSignalDispatchAvailable: true,
pcntlSignalAvailable: true,
pcntlWaitAvailable: true,
pcntlWaitStatusAvailable: true,
posixKillAvailable: true,
);

self::assertTrue($capabilities->supportsWorkerPool());
self::assertTrue($capabilities->supportsEventLoopSignals());
}

#[Test]
public function missingPcntlFunctionsDisableWorkerPoolOnly(): void
{
$capabilities = new ServeRuntimeCapabilities(
directorySeparator: '/',
sigintDefined: true,
sigtermDefined: true,
pcntlAsyncSignalsAvailable: false,
pcntlForkAvailable: true,
pcntlSignalDispatchAvailable: true,
pcntlSignalAvailable: true,
pcntlWaitAvailable: true,
pcntlWaitStatusAvailable: true,
posixKillAvailable: true,
);

self::assertFalse($capabilities->supportsWorkerPool());
self::assertTrue($capabilities->supportsEventLoopSignals());
}

#[Test]
public function missingPcntlSignalDispatchDisablesEventLoopSignals(): void
{
$capabilities = new ServeRuntimeCapabilities(
directorySeparator: '/',
sigintDefined: true,
sigtermDefined: true,
pcntlAsyncSignalsAvailable: true,
pcntlForkAvailable: true,
pcntlSignalDispatchAvailable: false,
pcntlSignalAvailable: true,
pcntlWaitAvailable: true,
pcntlWaitStatusAvailable: true,
posixKillAvailable: true,
);

self::assertFalse($capabilities->supportsWorkerPool());
self::assertFalse($capabilities->supportsEventLoopSignals());
}
}
Loading