From 458c082afa712bdc3a43c9e04efc1b3147cc488a Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Fri, 7 Aug 2026 19:29:54 +0200 Subject: [PATCH 1/5] Add dev build tool for PHP manual across different manuals --- .docker/Dockerfile | 21 ++ .gitignore | 4 + dev.php | 17 ++ docbookcs.dev.xml | 44 ++++ docs/local-setup.md | 2 +- scripts/dev/Application.php | 163 ++++++++++++++ scripts/dev/Command/BuildCommand.php | 20 ++ scripts/dev/Command/Command.php | 12 + scripts/dev/Command/ConfigureCommand.php | 38 ++++ scripts/dev/Command/HelpCommand.php | 47 ++++ scripts/dev/Command/LintCommand.php | 57 +++++ scripts/dev/Command/PullCommand.php | 31 +++ scripts/dev/Command/RenderCommand.php | 59 +++++ scripts/dev/Command/ServeCommand.php | 37 +++ scripts/dev/Command/ShellCommand.php | 20 ++ scripts/dev/Environment/DockerEnvironment.php | 196 ++++++++++++++++ scripts/dev/Environment/Environment.php | 34 +++ scripts/dev/Environment/LocalEnvironment.php | 131 +++++++++++ scripts/dev/Options.php | 21 ++ scripts/dev/ProcessRunner.php | 63 ++++++ scripts/dev/Workspace.php | 213 ++++++++++++++++++ 21 files changed, 1229 insertions(+), 1 deletion(-) create mode 100644 .docker/Dockerfile create mode 100755 dev.php create mode 100644 docbookcs.dev.xml create mode 100644 scripts/dev/Application.php create mode 100644 scripts/dev/Command/BuildCommand.php create mode 100644 scripts/dev/Command/Command.php create mode 100644 scripts/dev/Command/ConfigureCommand.php create mode 100644 scripts/dev/Command/HelpCommand.php create mode 100644 scripts/dev/Command/LintCommand.php create mode 100644 scripts/dev/Command/PullCommand.php create mode 100644 scripts/dev/Command/RenderCommand.php create mode 100644 scripts/dev/Command/ServeCommand.php create mode 100644 scripts/dev/Command/ShellCommand.php create mode 100644 scripts/dev/Environment/DockerEnvironment.php create mode 100644 scripts/dev/Environment/Environment.php create mode 100644 scripts/dev/Environment/LocalEnvironment.php create mode 100644 scripts/dev/Options.php create mode 100644 scripts/dev/ProcessRunner.php create mode 100644 scripts/dev/Workspace.php diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000000..5d7552229f --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,21 @@ +FROM php:8.4-cli + +ARG UID=1000 +ARG GID=1000 + +RUN apt-get update && \ + apt-get install -y git default-jre-headless + +WORKDIR /var/www + +ADD https://api.github.com/repos/php/phd/git/refs/heads/master version-phd.json +ADD https://api.github.com/repos/php/docbook-cs/git/refs/heads/main version-docbook-cs.json + +RUN echo 'memory_limit = 512M' >> /usr/local/etc/php/conf.d/local.ini + +RUN chown $UID:$GID /var/www + +USER $UID:$GID + +RUN git clone --depth 1 https://github.com/php/phd.git && \ + git clone --depth 1 https://github.com/php/docbook-cs.git diff --git a/.gitignore b/.gitignore index 8e5220c8b2..c11692a3b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Files generated by the configure script .manual.xml +.manual.*.xml .revcheck.json version.xml sources.xml @@ -9,3 +10,6 @@ fileModHistory.php # A plece for all temporary or generated files (idempotent build) temp/ + +# Docker dev build stamp +.docker/built diff --git a/dev.php b/dev.php new file mode 100755 index 0000000000..eafbf2bf19 --- /dev/null +++ b/dev.php @@ -0,0 +1,17 @@ +#!/usr/bin/env php +run(array_slice($argv, 1))); diff --git a/docbookcs.dev.xml b/docbookcs.dev.xml new file mode 100644 index 0000000000..b6405720b3 --- /dev/null +++ b/docbookcs.dev.xml @@ -0,0 +1,44 @@ + + + + + + @LANG@ + doc-base + + + + + + + + + + + + . + + + + extensions.ent + language-defs.ent + language-snippets.ent + entities/ + ../doc-base/entities/ + ../doc-base/temp/manual.ent + ../doc-base/temp/entities.ent + ../doc-base/temp/file-entities.ent + ../doc-base/temp/file-entities + + + + output/* + + + diff --git a/docs/local-setup.md b/docs/local-setup.md index 103d9add29..b00e820dbb 100644 --- a/docs/local-setup.md +++ b/docs/local-setup.md @@ -133,4 +133,4 @@ revert the changes with commands below and open an issue on git config --unset core.autocrlf git reset git status -``` \ No newline at end of file +``` diff --git a/scripts/dev/Application.php b/scripts/dev/Application.php new file mode 100644 index 0000000000..6874a9350e --- /dev/null +++ b/scripts/dev/Application.php @@ -0,0 +1,163 @@ + $args Command line arguments, without argv[0]. + */ + public function run(array $args): int + { + $options = new Options(); + $command = $this->parse($args, $options); + + if ($command === null) { + return 1; + } + + if ($command === 'help') { + return (new HelpCommand())->execute($options); + } + + $subcommand = null; + + if ($command === 'docker') { + $subcommand = array_shift($options->args); + + if (!in_array($subcommand, ['build', 'shell'], true)) { + fwrite(STDERR, "Usage: php dev.php docker (see: php dev.php help)\n"); + return 1; + } + + $options->docker = true; + } + + if ($command === 'render') { + $subcommand = array_shift($options->args); + + if (!in_array($subcommand, ['xhtml', 'php'], true)) { + fwrite(STDERR, "Usage: php dev.php render (see: php dev.php help)\n"); + return 1; + } + + $options->format = $subcommand; + } + + if ($command === 'cs') { + $subcommand = array_shift($options->args); + + if (!in_array($subcommand, ['lint', 'fix'], true)) { + fwrite(STDERR, "Usage: php dev.php cs (see: php dev.php help)\n"); + return 1; + } + } + + $runner = new ProcessRunner(); + $dockerAvailable = $runner->runQuiet(['docker', 'version', '--format', '{{.Server.Version}}']) === 0; + + if ($options->docker === true && !$dockerAvailable) { + fwrite(STDERR, "error: Docker requested but the docker command is not available.\n"); + return 1; + } + + $workspace = new Workspace($this->basedir, $runner, $options->assumeYes); + $environment = ($options->docker ?? $dockerAvailable) + ? new DockerEnvironment($workspace, $runner) + : new LocalEnvironment($workspace, $runner); + + $configure = new ConfigureCommand($workspace, $environment); + + switch ($command) { + case 'pull': + return (new PullCommand($workspace, $environment))->execute($options); + case 'configure': + return $configure->execute($options); + case 'render': + return (new RenderCommand($workspace, $environment, $configure))->execute($options); + case 'cs': + return (new LintCommand($workspace, $environment, $configure, fix: $subcommand === 'fix')) + ->execute($options); + case 'serve': + return (new ServeCommand($workspace, $environment))->execute($options); + case 'docker': + return $subcommand === 'build' + ? (new BuildCommand($environment))->execute($options) + : (new ShellCommand($environment))->execute($options); + } + + fwrite(STDERR, "Unknown command: $command (see: php dev.php help)\n"); + + return 1; + } + + /** @param list $args */ + private function parse(array $args, Options $options): ?string + { + $command = null; + + foreach ($args as $arg) { + if (preg_match('/^--lang=(.+)$/', $arg, $m)) { + $options->lang = $m[1]; + continue; + } + + if (preg_match('/^--port=(\d+)$/', $arg, $m)) { + $options->port = (int) $m[1]; + continue; + } + + if ($arg === '--docker') { + $options->docker = true; + continue; + } + + if ($arg === '--no-docker') { + $options->docker = false; + continue; + } + + if ($arg === '--yes' || $arg === '-y') { + $options->assumeYes = true; + continue; + } + + if ($command === null) { + if ($arg[0] !== '-') { + $command = $arg; + continue; + } + + if ($arg === '-h' || $arg === '--help') { + $command = 'help'; + continue; + } + + $fileName = $_SERVER['SCRIPT_FILENAME']; + fwrite(STDERR, "Unknown option: $arg (see: php $fileName help)\n"); + return null; + } + + $options->args[] = $arg; + } + + return $command ?? 'help'; + } +} diff --git a/scripts/dev/Command/BuildCommand.php b/scripts/dev/Command/BuildCommand.php new file mode 100644 index 0000000000..eae7883836 --- /dev/null +++ b/scripts/dev/Command/BuildCommand.php @@ -0,0 +1,20 @@ +environment->buildImage(); + } +} diff --git a/scripts/dev/Command/Command.php b/scripts/dev/Command/Command.php new file mode 100644 index 0000000000..e3e0866727 --- /dev/null +++ b/scripts/dev/Command/Command.php @@ -0,0 +1,12 @@ +lang; + + if (!$this->workspace->ensureLangRepos($lang, $this->environment->canMapDirectoryNames())) { + return 1; + } + + $this->workspace->pullSideRepos($lang); + + $args = array_merge([ + ($this->workspace->isBaseLang($lang) ? '--with-base-lang=' : '--with-lang=') . $lang, + '--enable-xml-details', + '--disable-libxml-check', + '--redirect-stderr-to-stdout', + ], $options->args); + + return $this->environment->configure($lang, $args); + } +} diff --git a/scripts/dev/Command/HelpCommand.php b/scripts/dev/Command/HelpCommand.php new file mode 100644 index 0000000000..bed76807ba --- /dev/null +++ b/scripts/dev/Command/HelpCommand.php @@ -0,0 +1,47 @@ + [options] [extra arguments] + + Commands: + pull Clone missing sibling repositories and update existing ones + configure Assemble and validate the manual, without rendering + render xhtml configure + render the chunked XHTML manual to /output + render php configure + render the web (PHP) version to /output + cs lint Run docbook-cs; extra arguments are passed through (paths, --wide) + cs fix Same as cs lint, with --fix: rewrite violations that have fixers + serve Serve /output over HTTP + docker build Build the Docker image + docker shell Interactive shell inside the container + + Options: + --lang=XX Language to operate on (default: en) + --port=NNNN Port for serve (default: 8080) + --docker Force Docker mode (default: used when available) + --no-docker Force local mode + --yes, -y Clone missing repositories without asking for confirmation + + Any other argument after the command is passed through: to configure.php + for configure/render (e.g. --with-partial=book.datetime), and to + docbook-cs for cs lint/cs fix (e.g. reference/datetime --wide). + + HELP; + + return 0; + } +} diff --git a/scripts/dev/Command/LintCommand.php b/scripts/dev/Command/LintCommand.php new file mode 100644 index 0000000000..b9ea6346bb --- /dev/null +++ b/scripts/dev/Command/LintCommand.php @@ -0,0 +1,57 @@ +lang; + + if (!$this->workspace->ensureLangRepos($lang, $this->environment->canMapDirectoryNames())) { + return 1; + } + + if ($this->configure->execute($options) !== 0) { + echo "\nconfigure reported problems (see above); linting anyway.\n\n"; + } + + $langdir = $this->workspace->langDir($lang); + $args = $options->args; + + if ($this->fix) { + array_unshift($args, '--fix'); + } + + if (!file_exists("$langdir/docbookcs.xml")) { + $template = $this->workspace->getDocbookcsConfig(); + + if ($template === null) { + return 1; + } + + $config = "$langdir/.docbookcs.dev.xml"; + file_put_contents($config, str_replace('@LANG@', $lang, $template)); + register_shutdown_function(static function () use ($config): void { + @unlink($config); + }); + array_unshift($args, '--config=.docbookcs.dev.xml'); + } + + return $this->environment->lint($lang, $args); + } +} diff --git a/scripts/dev/Command/PullCommand.php b/scripts/dev/Command/PullCommand.php new file mode 100644 index 0000000000..2a763c8f01 --- /dev/null +++ b/scripts/dev/Command/PullCommand.php @@ -0,0 +1,31 @@ +environment->canMapDirectoryNames(); + + if (!$this->workspace->ensureLangRepos($options->lang, $mapNames)) { + return 1; + } + + $this->workspace->pullSideRepos($options->lang, verbose: true); + + return 0; + } +} diff --git a/scripts/dev/Command/RenderCommand.php b/scripts/dev/Command/RenderCommand.php new file mode 100644 index 0000000000..fe4f70c22d --- /dev/null +++ b/scripts/dev/Command/RenderCommand.php @@ -0,0 +1,59 @@ +configure->execute($options); + + if ($ret !== 0) { + return $ret; + } + + $lang = $options->lang; + $format = $options->format; + + // PhD never cleans its output directory, so files from removed or + // renamed pages would linger forever. Remove this format's own + // output tree before rendering; other formats next to it are kept. + $stale = $this->workspace->langDir($lang) . '/output/' + . ($format === 'php' ? 'php-web' : 'php-chunked-xhtml'); + + if (is_dir($stale)) { + echo "Removing previous $stale\n"; + $this->workspace->removeTree($stale); + } + + return $this->environment->render($lang, $this->docbook($options), $format); + } + + /** + * configure.php writes a partial build (--with-partial=xml-id) to + * .manual..xml, next to the always-written full .manual.xml. + */ + private function docbook(Options $options): string + { + foreach ($options->args as $arg) { + if (preg_match('/^--with-partial=(.+)$/', $arg, $m)) { + return '.manual.' . $m[1] . '.xml'; + } + } + + return '.manual.xml'; + } +} diff --git a/scripts/dev/Command/ServeCommand.php b/scripts/dev/Command/ServeCommand.php new file mode 100644 index 0000000000..83b5a768a2 --- /dev/null +++ b/scripts/dev/Command/ServeCommand.php @@ -0,0 +1,37 @@ +lang; + $output = $this->workspace->langDir($lang) . '/output'; + + if (!is_dir($output)) { + echo "Note: $output does not exist yet; run \"php dev.php render xhtml --lang=$lang\" first.\n"; + } + + // PhD renders each format into its own subdirectory of output/. + // Serve the chunked XHTML tree directly, so http://localhost:/ + // lands on its index page instead of a 404. + $subdir = is_dir("$output/php-chunked-xhtml") ? '/php-chunked-xhtml' : ''; + + echo "Serving the $lang manual at http://localhost:{$options->port}/ (Ctrl-C to stop)\n"; + + return $this->environment->serve($lang, $options->port, $subdir); + } +} diff --git a/scripts/dev/Command/ShellCommand.php b/scripts/dev/Command/ShellCommand.php new file mode 100644 index 0000000000..f7f9e60ea5 --- /dev/null +++ b/scripts/dev/Command/ShellCommand.php @@ -0,0 +1,20 @@ +environment->shell($options->lang); + } +} diff --git a/scripts/dev/Environment/DockerEnvironment.php b/scripts/dev/Environment/DockerEnvironment.php new file mode 100644 index 0000000000..4a3aeb40a7 --- /dev/null +++ b/scripts/dev/Environment/DockerEnvironment.php @@ -0,0 +1,196 @@ +ensureImage()) { + return 1; + } + + return $this->dockerRun($lang, array_merge(['php', 'doc-base/configure.php'], $args)); + } + + public function render(string $lang, string $docbook, string $format): int + { + if (!$this->ensureImage()) { + return 1; + } + + return $this->dockerRun($lang, [ + 'php', + 'phd/render.php', + '--docbook', + "doc-base/$docbook", + '--output=/var/www/' . $lang . '/output', + '--package', + 'PHP', + '--format', + $format, + ]); + } + + public function lint(string $lang, array $args): int + { + if (!$this->ensureImage()) { + return 1; + } + + return $this->dockerRun( + $lang, + array_merge(['php', '/var/www/docbook-cs/bin/docbook-cs'], $args), + [ + '-e', + 'GIT_CONFIG_COUNT=1', + '-e', + 'GIT_CONFIG_KEY_0=safe.directory', + '-e', + 'GIT_CONFIG_VALUE_0=*', + ], + "/var/www/$lang" + ); + } + + public function serve(string $lang, int $port, string $subdir): int + { + if (!$this->ensureImage()) { + return 1; + } + + // Inside the container the server must bind 0.0.0.0 to be reachable + // through the published port; the host side stays localhost-only. + return $this->dockerRun( + $lang, + ['php', '-S', "0.0.0.0:$port", '-t', "/var/www/$lang/output$subdir"], + ['-p', "127.0.0.1:$port:$port"] + ); + } + + public function shell(string $lang): int + { + if (!$this->ensureImage()) { + return 1; + } + + return $this->dockerRun($lang, ['bash'], ['-it']); + } + + public function buildImage(): int + { + return $this->build() ? 0 : 1; + } + + private function ensureImage(): bool + { + $dockerfile = $this->workspace->basedir() . '/.docker/Dockerfile'; + $stamp = $this->workspace->basedir() . '/.docker/built'; + + if ( + $this->runner->runQuiet(['docker', 'image', 'inspect', self::IMAGE]) === 0 + && file_exists($stamp) + && filemtime($stamp) >= filemtime($dockerfile) + ) { + return true; + } + + return $this->build(); + } + + private function build(): bool + { + $cmd = ['docker', 'build']; + $ids = $this->unixIds(); + + if ($ids !== null) { + array_push($cmd, '--build-arg', 'UID=' . $ids[0]); + array_push($cmd, '--build-arg', 'GID=' . $ids[1]); + } + + array_push($cmd, '-t', self::IMAGE, $this->workspace->basedir() . '/.docker'); + + if ($this->runner->run($cmd) !== 0) { + return false; + } + + touch($this->workspace->basedir() . '/.docker/built'); + + return true; + } + + private function mounts(string $lang): array + { + $root = $this->workspace->rootdir(); + $mounts = [realpath($this->workspace->basedir()) => '/var/www/doc-base']; + + $mounts[$this->workspace->langDir($lang)] = "/var/www/$lang"; + + if (!$this->workspace->isBaseLang($lang)) { + $mounts[$this->workspace->langDir('en')] = '/var/www/en'; + } + + foreach (['phd', 'docbook-cs'] as $tool) { + if (is_dir("$root/$tool")) { + $mounts[realpath("$root/$tool")] = "/var/www/$tool"; + } + } + + return $mounts; + } + + /** + * @param list $inner Command to run inside the container. + * @param list $extra Extra docker run arguments. + */ + private function dockerRun(string $lang, array $inner, array $extra = [], string $workdir = '/var/www'): int + { + // --init: without it the command runs as PID 1, which ignores + // SIGINT, so Ctrl-C would leave the container running forever. + $cmd = ['docker', 'run', '--rm', '--init']; + + foreach ($this->mounts($lang) as $host => $container) { + array_push($cmd, '-v', "$host:$container"); + } + + array_push($cmd, '-w', $workdir); + $ids = $this->unixIds(); + + if ($ids !== null) { + array_push($cmd, '-u', $ids[0] . ':' . $ids[1]); + } + + $cmd = array_merge($cmd, $extra); + $cmd[] = self::IMAGE; + + return $this->runner->run(array_merge($cmd, $inner)); + } + + /** @return array{int, int}|null */ + private function unixIds(): ?array + { + if (function_exists('posix_getuid')) { + return [posix_getuid(), posix_getgid()]; + } + + return null; + } +} diff --git a/scripts/dev/Environment/Environment.php b/scripts/dev/Environment/Environment.php new file mode 100644 index 0000000000..4bba06ad3b --- /dev/null +++ b/scripts/dev/Environment/Environment.php @@ -0,0 +1,34 @@ + checkout can be presented under its language + * name without touching the filesystem (Docker mounts can). + */ + public function canMapDirectoryNames(): bool; + + /** @param list $args configure.php arguments */ + public function configure(string $lang, array $args): int; + + /** @param string $docbook Manual file name inside doc-base, e.g. ".manual.xml". */ + public function render(string $lang, string $docbook, string $format): int; + + /** @param list $args docbook-cs arguments; runs in the language directory */ + public function lint(string $lang, array $args): int; + + /** @param string $subdir Path inside /output to use as web root, or "". */ + public function serve(string $lang, int $port, string $subdir): int; + + public function shell(string $lang): int; + + public function buildImage(): int; +} diff --git a/scripts/dev/Environment/LocalEnvironment.php b/scripts/dev/Environment/LocalEnvironment.php new file mode 100644 index 0000000000..43d2aa2e9c --- /dev/null +++ b/scripts/dev/Environment/LocalEnvironment.php @@ -0,0 +1,131 @@ +requireLocalPhp(80400, 'building the manual')) { + return 1; + } + + $args[] = '--with-php=' . PHP_BINARY; + + return $this->runner->run( + array_merge([PHP_BINARY, $this->workspace->basedir() . '/configure.php'], $args), + $this->workspace->rootdir() + ); + } + + public function render(string $lang, string $docbook, string $format): int + { + $root = $this->workspace->rootdir(); + + if (!$this->workspace->ensureRepo("$root/phd", 'https://github.com/php/phd.git')) { + return 1; + } + + return $this->runner->run([ + PHP_BINARY, + "$root/phd/render.php", + '--docbook', + $this->workspace->basedir() . '/' . $docbook, + '--output=' . $this->workspace->langDir($lang) . '/output', + '--package', + 'PHP', + '--format', + $format, + ], $root); + } + + public function lint(string $lang, array $args): int + { + if (!$this->requireLocalPhp(80500, 'docbook-cs')) { + return 1; + } + + $csdir = $this->workspace->rootdir() . '/docbook-cs'; + + if (!$this->workspace->ensureRepo($csdir, 'https://github.com/php/docbook-cs.git')) { + return 1; + } + + $langdir = $this->workspace->langDir($lang); + + if (file_exists("$csdir/vendor/autoload.php")) { + return $this->runner->run( + array_merge([PHP_BINARY, "$csdir/bin/docbook-cs"], $args), + $langdir + ); + } + + // No composer install needed: docbook-cs has no runtime + // dependencies, so a plain PSR-4 autoloader on src/ is enough to + // run it in-process. + spl_autoload_register(static function (string $class) use ($csdir): void { + if (str_starts_with($class, 'DocbookCS\\')) { + require $csdir . '/src/' . str_replace('\\', '/', substr($class, 10)) . '.php'; + } + }); + + chdir($langdir); + array_unshift($args, 'docbook-cs'); + + return \DocbookCS\Application::withArguments($args)->run(); + } + + public function serve(string $lang, int $port, string $subdir): int + { + return $this->runner->run([ + PHP_BINARY, + '-S', + "localhost:$port", + '-t', + $this->workspace->langDir($lang) . '/output' . $subdir, + ]); + } + + public function shell(string $lang): int + { + fwrite(STDERR, "error: docker shell requires Docker.\n"); + + return 1; + } + + public function buildImage(): int + { + fwrite(STDERR, "error: docker build requires Docker.\n"); + + return 1; + } + + private function requireLocalPhp(int $minimum, string $what): bool + { + if (PHP_VERSION_ID >= $minimum) { + return true; + } + + $need = sprintf('%d.%d', intdiv($minimum, 10000), intdiv($minimum % 10000, 100)); + fwrite(STDERR, "error: $what requires PHP $need+ without Docker (this is PHP " + . PHP_VERSION . "). Install Docker or a newer PHP.\n"); + + return false; + } +} diff --git a/scripts/dev/Options.php b/scripts/dev/Options.php new file mode 100644 index 0000000000..6aa0fd43d6 --- /dev/null +++ b/scripts/dev/Options.php @@ -0,0 +1,21 @@ + */ + public array $args = []; +} diff --git a/scripts/dev/ProcessRunner.php b/scripts/dev/ProcessRunner.php new file mode 100644 index 0000000000..c5017aa190 --- /dev/null +++ b/scripts/dev/ProcessRunner.php @@ -0,0 +1,63 @@ + $cmd + * @param array $env Extra environment variables. + */ + public function run(array $cmd, ?string $cwd = null, array $env = []): int + { + $envp = $env === [] ? null : array_merge(getenv(), $env); + $proc = @proc_open($cmd, [STDIN, STDOUT, STDERR], $pipes, $cwd, $envp); + + if (!is_resource($proc)) { + fwrite(STDERR, "error: failed to execute {$cmd[0]}.\n"); + return 127; + } + + return proc_close($proc); + } + + /** @param list $cmd */ + public function output(array $cmd, ?string $cwd = null): ?string + { + $spec = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']]; + $proc = @proc_open($cmd, $spec, $pipes, $cwd); + + if (!is_resource($proc)) { + return null; + } + + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + stream_get_contents($pipes[2]); + fclose($pipes[2]); + + return proc_close($proc) === 0 ? (string) $stdout : null; + } + + /** @param list $cmd */ + public function runQuiet(array $cmd): int + { + $spec = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']]; + $proc = @proc_open($cmd, $spec, $pipes); + + if (!is_resource($proc)) { + return 127; /* command not found */ + } + + fclose($pipes[0]); + stream_get_contents($pipes[1]); + fclose($pipes[1]); + stream_get_contents($pipes[2]); + fclose($pipes[2]); + + return proc_close($proc); + } +} diff --git a/scripts/dev/Workspace.php b/scripts/dev/Workspace.php new file mode 100644 index 0000000000..bfc3ba80e7 --- /dev/null +++ b/scripts/dev/Workspace.php @@ -0,0 +1,213 @@ +basedir; + } + + public function rootdir(): string + { + return dirname($this->basedir); + } + + public function isBaseLang(string $lang): bool + { + return in_array($lang, ['extensions', 'en'], true); + } + + public function langDir(string $lang): string + { + $root = $this->rootdir(); + + if (is_dir("$root/$lang")) { + return realpath("$root/$lang"); + } + + return realpath("$root/doc-$lang") ?: "$root/$lang"; + } + + public function ensureRepo(string $dir, string $url): bool + { + if (is_dir($dir)) { + return true; + } + + if (!$this->confirm("Clone $url\n into $dir?")) { + fwrite(STDERR, "error: cannot continue without $dir.\n"); + return false; + } + + return $this->runner->run(['git', 'clone', $url, $dir]) === 0; + } + + /** + * @param bool $mapNames Whether the environment can present a + * doc- checkout under its language name + * (Docker mounts can, local builds cannot). + */ + public function ensureLang(string $lang, bool $mapNames): bool + { + $root = $this->rootdir(); + + if (is_dir("$root/$lang")) { + return true; + } + + if (is_dir("$root/doc-$lang")) { + if ($mapNames) { + return true; + } + + if (@symlink("doc-$lang", "$root/$lang")) { + echo "Created symlink $lang -> doc-$lang\n"; + return true; + } + + fwrite(STDERR, "error: found $root/doc-$lang but could not create a '$lang' " + . "symlink next to it; rename the directory to '$lang' or use Docker.\n"); + return false; + } + + $repo = 'doc-' . strtolower($lang); + + return $this->ensureRepo("$root/$lang", "https://github.com/php/$repo.git"); + } + + public function ensureLangRepos(string $lang, bool $mapNames): bool + { + if (!$this->isBaseLang($lang) && !$this->ensureLang('en', $mapNames)) { + return false; + } + + if ($lang === 'en') { + return $this->ensureLang('en', $mapNames); + } + + return $this->ensureLang($lang, $mapNames); + } + + public function pullSideRepos(string $lang, bool $verbose = false): void + { + $root = $this->rootdir(); + $repos = [$this->basedir]; + + if (!$this->isBaseLang($lang)) { + $repos[] = $this->langDir('en'); + } + + foreach (['phd', 'docbook-cs'] as $tool) { + if (is_dir("$root/$tool")) { + $repos[] = realpath("$root/$tool"); + } + } + + foreach ($repos as $repo) { + $this->pullRepo($repo, $verbose); + } + } + + private function pullRepo(string $dir, bool $verbose): void + { + if (!is_dir("$dir/.git")) { + return; + } + + $name = basename($dir); + $branch = trim((string) $this->runner->output(['git', '-C', $dir, 'rev-parse', '--abbrev-ref', 'HEAD'])); + + if (!in_array($branch, ['master', 'main'], true)) { + if ($verbose) { + echo "Not updating $name: on branch '$branch'.\n"; + } + + return; + } + + $before = $this->runner->output(['git', '-C', $dir, 'rev-parse', 'HEAD']); + + if ($this->runner->run(['git', '-C', $dir, 'pull', '--ff-only', '--quiet', 'origin', $branch]) !== 0) { + echo "Could not update $name; continuing with the current checkout.\n"; + return; + } + + $after = $this->runner->output(['git', '-C', $dir, 'rev-parse', 'HEAD']); + + if ($before !== $after) { + echo "Updated $name.\n"; + } elseif ($verbose) { + echo "$name is up to date.\n"; + } + } + + public function removeTree(string $dir): void + { + $items = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($items as $item) { + if ($item->isDir() && !$item->isLink()) { + rmdir($item->getPathname()); + } else { + unlink($item->getPathname()); + } + } + + rmdir($dir); + } + + public function getDocbookcsConfig(): ?string + { + $template = $this->basedir . '/docbookcs.dev.xml'; + $config = @file_get_contents($template); + + if ($config === false) { + fwrite(STDERR, "error: cannot read $template.\n"); + return null; + } + + return $config; + } + + private function confirm(string $question): bool + { + if ($this->assumeYes) { + return true; + } + + if (!stream_isatty(STDIN)) { + fwrite(STDERR, "error: confirmation needed but there is no terminal; re-run with --yes.\n"); + return false; + } + + echo $question . ' [Y/n] '; + $line = fgets(STDIN); + + // EOF (Ctrl-D, closed stdin) is not consent. + if ($line === false) { + echo "\n"; + return false; + } + + $answer = strtolower(trim($line)); + return in_array($answer, ['', 'y', 'yes']); + } +} From 8c373e355cb265aa13e2dd64f5fa2a43cfe8934e Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Tue, 11 Aug 2026 09:45:42 +0200 Subject: [PATCH 2/5] Add support for serving web-doc --- scripts/dev/Application.php | 12 ++- scripts/dev/Command/HelpCommand.php | 24 ++--- scripts/dev/Command/WebDocServeCommand.php | 98 +++++++++++++++++++ scripts/dev/Environment/DockerEnvironment.php | 98 ++++++++++++++++--- scripts/dev/Environment/Environment.php | 11 +++ scripts/dev/Environment/LocalEnvironment.php | 32 ++++++ scripts/dev/Workspace.php | 58 ++++++++++- 7 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 scripts/dev/Command/WebDocServeCommand.php diff --git a/scripts/dev/Application.php b/scripts/dev/Application.php index 6874a9350e..17840049d4 100644 --- a/scripts/dev/Application.php +++ b/scripts/dev/Application.php @@ -12,6 +12,7 @@ use PhpDoc\Dev\Command\RenderCommand; use PhpDoc\Dev\Command\ServeCommand; use PhpDoc\Dev\Command\ShellCommand; +use PhpDoc\Dev\Command\WebDocServeCommand; use PhpDoc\Dev\Environment\DockerEnvironment; use PhpDoc\Dev\Environment\LocalEnvironment; @@ -61,6 +62,13 @@ public function run(array $args): int $options->format = $subcommand; } + // "serve" takes an optional subject: plain serve shows the rendered + // manual, "serve web-doc" runs the doc.php.net site. + if ($command === 'serve' && ($options->args[0] ?? null) === 'web-doc') { + array_shift($options->args); + $subcommand = 'web-doc'; + } + if ($command === 'cs') { $subcommand = array_shift($options->args); @@ -96,7 +104,9 @@ public function run(array $args): int return (new LintCommand($workspace, $environment, $configure, fix: $subcommand === 'fix')) ->execute($options); case 'serve': - return (new ServeCommand($workspace, $environment))->execute($options); + return $subcommand === 'web-doc' + ? (new WebDocServeCommand($workspace, $environment))->execute($options) + : (new ServeCommand($workspace, $environment))->execute($options); case 'docker': return $subcommand === 'build' ? (new BuildCommand($environment))->execute($options) diff --git a/scripts/dev/Command/HelpCommand.php b/scripts/dev/Command/HelpCommand.php index bed76807ba..e70cba6000 100644 --- a/scripts/dev/Command/HelpCommand.php +++ b/scripts/dev/Command/HelpCommand.php @@ -12,22 +12,24 @@ public function execute(Options $options): int { echo << [options] [extra arguments] Commands: - pull Clone missing sibling repositories and update existing ones - configure Assemble and validate the manual, without rendering - render xhtml configure + render the chunked XHTML manual to /output - render php configure + render the web (PHP) version to /output - cs lint Run docbook-cs; extra arguments are passed through (paths, --wide) - cs fix Same as cs lint, with --fix: rewrite violations that have fixers - serve Serve /output over HTTP - docker build Build the Docker image - docker shell Interactive shell inside the container + pull Clone missing sibling repositories and update existing ones + configure Assemble and validate the manual, without rendering + render xhtml configure + render the chunked XHTML manual to /output + render php configure + render the web (PHP) version to /output + cs lint Run docbook-cs; extra arguments are passed through (paths, --wide) + cs fix Same as cs lint, with --fix: rewrite violations that have fixers + serve Serve /output over HTTP + serve web-doc Run a local doc.php.net site from the web-doc checkout + docker build Build the Docker image + docker shell Interactive shell inside the container Options: --lang=XX Language to operate on (default: en) diff --git a/scripts/dev/Command/WebDocServeCommand.php b/scripts/dev/Command/WebDocServeCommand.php new file mode 100644 index 0000000000..2b005712d0 --- /dev/null +++ b/scripts/dev/Command/WebDocServeCommand.php @@ -0,0 +1,98 @@ +environment->canMapDirectoryNames(); + + // The site needs en at request time (revcheck shells out to git in + // it), web-doc itself, and web-shared inside it. + if (!$this->workspace->ensureWebDocRepos() || !$this->workspace->ensureLang('en', $mapNames)) { + return 1; + } + + $sqlite = $this->workspace->webDocDir() . '/sqlite/status.sqlite'; + + if (!is_file($sqlite)) { + $generate = $this->workspace->confirm( + "Generate $sqlite\n (translation status data; parses git history and can take minutes)?" + ); + + if ($generate) { + if ($this->generateDb($options, $mapNames) !== 0) { + return 1; + } + } else { + echo "Note: serving without status.sqlite; translation status pages will be empty.\n"; + } + } + + echo "Serving doc.php.net at http://localhost:{$options->port}/ (Ctrl-C to stop)\n"; + + return $this->environment->serveWebDoc($options->port); + } + + private function generateDb(Options $options, bool $mapNames): int + { + $langs = array_keys($this->workspace->translationCheckouts()); + + if ($langs === [] && !$this->workspace->isBaseLang($options->lang)) { + if (!$this->workspace->ensureLang($options->lang, $mapNames)) { + return 1; + } + + $langs = [$options->lang]; + } + + if ($langs === []) { + echo "Note: no translation checkouts found; skipping status.sqlite " + . "(clone one, or pass --lang=XX, and re-run).\n"; + return 0; + } + + if ($this->workspace->isShallowRepo($this->workspace->langDir('en'))) { + echo "Note: the en checkout has shallow git history; status data will be " + . "incomplete (fix with: git -C " . $this->workspace->langDir('en') . " fetch --unshallow).\n"; + } + + $sqliteDir = $this->workspace->webDocDir() . '/sqlite'; + @mkdir($sqliteDir); + @unlink("$sqliteDir/status.sqlite.new"); + + echo 'Generating translation status data for: ' . implode(', ', $langs) . "\n"; + + if ($this->environment->generateRevisionDb($langs) !== 0) { + return 1; + } + + // genrevdb can exit 0 even on failure, so also judge success by its + // output file; renaming afterwards keeps a half-written database from + // ever being published under the name the site reads. + if (!is_file("$sqliteDir/status.sqlite.new")) { + fwrite(STDERR, "error: generating status.sqlite failed.\n"); + return 1; + } + + if (!rename("$sqliteDir/status.sqlite.new", "$sqliteDir/status.sqlite")) { + fwrite(STDERR, "error: could not move status.sqlite.new into place.\n"); + return 1; + } + + return 0; + } +} diff --git a/scripts/dev/Environment/DockerEnvironment.php b/scripts/dev/Environment/DockerEnvironment.php index 4a3aeb40a7..090ea0a3b7 100644 --- a/scripts/dev/Environment/DockerEnvironment.php +++ b/scripts/dev/Environment/DockerEnvironment.php @@ -28,7 +28,7 @@ public function configure(string $lang, array $args): int return 1; } - return $this->dockerRun($lang, array_merge(['php', 'doc-base/configure.php'], $args)); + return $this->dockerRun($this->mounts($lang), array_merge(['php', 'doc-base/configure.php'], $args)); } public function render(string $lang, string $docbook, string $format): int @@ -37,7 +37,7 @@ public function render(string $lang, string $docbook, string $format): int return 1; } - return $this->dockerRun($lang, [ + return $this->dockerRun($this->mounts($lang), [ 'php', 'phd/render.php', '--docbook', @@ -57,16 +57,9 @@ public function lint(string $lang, array $args): int } return $this->dockerRun( - $lang, + $this->mounts($lang), array_merge(['php', '/var/www/docbook-cs/bin/docbook-cs'], $args), - [ - '-e', - 'GIT_CONFIG_COUNT=1', - '-e', - 'GIT_CONFIG_KEY_0=safe.directory', - '-e', - 'GIT_CONFIG_VALUE_0=*', - ], + $this->gitSafeDirectoryEnv(), "/var/www/$lang" ); } @@ -80,19 +73,63 @@ public function serve(string $lang, int $port, string $subdir): int // Inside the container the server must bind 0.0.0.0 to be reachable // through the published port; the host side stays localhost-only. return $this->dockerRun( - $lang, + $this->mounts($lang), ['php', '-S', "0.0.0.0:$port", '-t', "/var/www/$lang/output$subdir"], ['-p', "127.0.0.1:$port:$port"] ); } + public function serveWebDoc(int $port): int + { + if (!$this->ensureImage()) { + return 1; + } + + // The site shells out to git inside the mounted checkouts at + // request time, hence the safe.directory override while serving. + return $this->dockerRun( + $this->webDocMounts(), + ['php', '-S', "0.0.0.0:$port", 'router.php'], + array_merge( + [ + '-p', + "127.0.0.1:$port:$port", + '-e', + 'PHPDOC_GIT_DIR=/var/www', + '-e', + 'SQLITE_DIR=/var/www/web-doc/sqlite', + '-e', + 'BASE_DOCS_PATH=/var/www/doc-base/docs', + ], + $this->gitSafeDirectoryEnv() + ), + '/var/www/web-doc' + ); + } + + public function generateRevisionDb(array $langs): int + { + if (!$this->ensureImage()) { + return 1; + } + + return $this->dockerRun( + $this->webDocMounts(), + array_merge( + ['php', 'doc-base/scripts/translation/genrevdb.php', 'web-doc/sqlite/status.sqlite.new'], + $langs + ), + $this->gitSafeDirectoryEnv() + ); + } + public function shell(string $lang): int { if (!$this->ensureImage()) { return 1; } - return $this->dockerRun($lang, ['bash'], ['-it']); + return $this->dockerRun($this->mounts($lang), ['bash'], ['-it']); } public function buildImage(): int @@ -157,17 +194,48 @@ private function mounts(string $lang): array return $mounts; } + private function webDocMounts(): array + { + $mounts = [ + realpath($this->workspace->basedir()) => '/var/www/doc-base', + $this->workspace->webDocDir() => '/var/www/web-doc', + $this->workspace->langDir('en') => '/var/www/en', + ]; + + // Mount every translation checkout so PHPDOC_GIT_DIR=/var/www looks + // like a full doc.php.net workspace to the site. + foreach ($this->workspace->translationCheckouts() as $lang => $dir) { + $mounts[$dir] = "/var/www/$lang"; + } + + return $mounts; + } + + /** @return list */ + private function gitSafeDirectoryEnv(): array + { + return [ + '-e', + 'GIT_CONFIG_COUNT=1', + '-e', + 'GIT_CONFIG_KEY_0=safe.directory', + '-e', + 'GIT_CONFIG_VALUE_0=*', + ]; + } + /** + * @param array $mounts Host path => container path. * @param list $inner Command to run inside the container. * @param list $extra Extra docker run arguments. */ - private function dockerRun(string $lang, array $inner, array $extra = [], string $workdir = '/var/www'): int + private function dockerRun(array $mounts, array $inner, array $extra = [], string $workdir = '/var/www'): int { // --init: without it the command runs as PID 1, which ignores // SIGINT, so Ctrl-C would leave the container running forever. $cmd = ['docker', 'run', '--rm', '--init']; - foreach ($this->mounts($lang) as $host => $container) { + foreach ($mounts as $host => $container) { array_push($cmd, '-v', "$host:$container"); } diff --git a/scripts/dev/Environment/Environment.php b/scripts/dev/Environment/Environment.php index 4bba06ad3b..434a64f666 100644 --- a/scripts/dev/Environment/Environment.php +++ b/scripts/dev/Environment/Environment.php @@ -28,6 +28,17 @@ public function lint(string $lang, array $args): int; /** @param string $subdir Path inside /output to use as web root, or "". */ public function serve(string $lang, int $port, string $subdir): int; + /** Serve the doc.php.net site from the web-doc checkout. */ + public function serveWebDoc(int $port): int; + + /** + * Run genrevdb.php from the workspace root, writing the translation + * status database to web-doc/sqlite/status.sqlite.new. + * + * @param list $langs Translation language codes. + */ + public function generateRevisionDb(array $langs): int; + public function shell(string $lang): int; public function buildImage(): int; diff --git a/scripts/dev/Environment/LocalEnvironment.php b/scripts/dev/Environment/LocalEnvironment.php index 43d2aa2e9c..8b64a1c662 100644 --- a/scripts/dev/Environment/LocalEnvironment.php +++ b/scripts/dev/Environment/LocalEnvironment.php @@ -102,6 +102,38 @@ public function serve(string $lang, int $port, string $subdir): int ]); } + public function serveWebDoc(int $port): int + { + $webdoc = $this->workspace->webDocDir(); + + // router.php resolves www/ relative to the working directory, so + // the server has to run from inside the web-doc checkout. + return $this->runner->run( + [PHP_BINARY, '-S', "localhost:$port", 'router.php'], + $webdoc, + [ + 'PHPDOC_GIT_DIR' => $this->workspace->rootdir(), + 'SQLITE_DIR' => "$webdoc/sqlite", + 'BASE_DOCS_PATH' => $this->workspace->basedir() . '/docs', + ] + ); + } + + public function generateRevisionDb(array $langs): int + { + if (!$this->requireLocalPhp(80100, 'generating status.sqlite')) { + return 1; + } + + // genrevdb resolves en and each language relative to the working + // directory, so it must run from the workspace root. + return $this->runner->run(array_merge([ + PHP_BINARY, + $this->workspace->basedir() . '/scripts/translation/genrevdb.php', + $this->workspace->webDocDir() . '/sqlite/status.sqlite.new', + ], $langs), $this->workspace->rootdir()); + } + public function shell(string $lang): int { fwrite(STDERR, "error: docker shell requires Docker.\n"); diff --git a/scripts/dev/Workspace.php b/scripts/dev/Workspace.php index bfc3ba80e7..73821ab5a6 100644 --- a/scripts/dev/Workspace.php +++ b/scripts/dev/Workspace.php @@ -43,6 +43,53 @@ public function langDir(string $lang): string return realpath("$root/doc-$lang") ?: "$root/$lang"; } + public function webDocDir(): string + { + $dir = $this->rootdir() . '/web-doc'; + + return realpath($dir) ?: $dir; + } + + public function ensureWebDocRepos(): bool + { + $dir = $this->webDocDir(); + + return $this->ensureRepo($dir, 'https://github.com/php/web-doc.git') + && $this->ensureRepo("$dir/shared", 'https://github.com/php/web-shared.git'); + } + + /** + * Language checkouts present in the workspace, i.e. sibling directories + * with a translation.xml (so en/doc-base are never included). + * + * @return array Language code => absolute directory. + */ + public function translationCheckouts(): array + { + $checkouts = []; + + foreach (glob($this->rootdir() . '/*', GLOB_ONLYDIR) ?: [] as $dir) { + if (!is_file("$dir/translation.xml")) { + continue; + } + + $lang = basename($dir); + $bare = !str_starts_with($lang, 'doc-'); + + if (!$bare) { + $lang = substr($lang, 4); + } + + // When both "xx" and "doc-xx" exist the bare name wins, + // matching langDir(). + if ($bare || !isset($checkouts[$lang])) { + $checkouts[$lang] = realpath($dir); + } + } + + return $checkouts; + } + public function ensureRepo(string $dir, string $url): bool { if (is_dir($dir)) { @@ -112,7 +159,7 @@ public function pullSideRepos(string $lang, bool $verbose = false): void $repos[] = $this->langDir('en'); } - foreach (['phd', 'docbook-cs'] as $tool) { + foreach (['phd', 'docbook-cs', 'web-doc', 'web-doc/shared'] as $tool) { if (is_dir("$root/$tool")) { $repos[] = realpath("$root/$tool"); } @@ -156,6 +203,13 @@ private function pullRepo(string $dir, bool $verbose): void } } + public function isShallowRepo(string $dir): bool + { + $out = $this->runner->output(['git', '-C', $dir, 'rev-parse', '--is-shallow-repository']); + + return trim((string) $out) === 'true'; + } + public function removeTree(string $dir): void { $items = new RecursiveIteratorIterator( @@ -187,7 +241,7 @@ public function getDocbookcsConfig(): ?string return $config; } - private function confirm(string $question): bool + public function confirm(string $question): bool { if ($this->assumeYes) { return true; From a7c60e3662c67e1a489207d1ddad18159ff577fc Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Tue, 11 Aug 2026 09:57:48 +0200 Subject: [PATCH 3/5] Rename dev.php to phpdoc.php --- dev.php => phpdoc.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dev.php => phpdoc.php (100%) diff --git a/dev.php b/phpdoc.php similarity index 100% rename from dev.php rename to phpdoc.php From 5bcad65f5664cf4064cc705378048c508d7609bf Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Tue, 11 Aug 2026 09:59:26 +0200 Subject: [PATCH 4/5] Rename dev.php to phpdoc.php --- phpdoc.php | 2 +- scripts/dev/Application.php | 8 ++++---- scripts/dev/Command/HelpCommand.php | 2 +- scripts/dev/Command/ServeCommand.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/phpdoc.php b/phpdoc.php index eafbf2bf19..e53b6e9cb1 100755 --- a/phpdoc.php +++ b/phpdoc.php @@ -3,7 +3,7 @@ /** * Dev build tool for the PHP manual, usable for every language - * Run "php dev.php help" for usage + * Run "php docdev.php help" for usage */ declare(strict_types=1); diff --git a/scripts/dev/Application.php b/scripts/dev/Application.php index 17840049d4..368ca6dbed 100644 --- a/scripts/dev/Application.php +++ b/scripts/dev/Application.php @@ -44,7 +44,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['build', 'shell'], true)) { - fwrite(STDERR, "Usage: php dev.php docker (see: php dev.php help)\n"); + fwrite(STDERR, "Usage: php docdev.php docker (see: php docdev.php help)\n"); return 1; } @@ -55,7 +55,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['xhtml', 'php'], true)) { - fwrite(STDERR, "Usage: php dev.php render (see: php dev.php help)\n"); + fwrite(STDERR, "Usage: php docdev.php render (see: php docdev.php help)\n"); return 1; } @@ -73,7 +73,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['lint', 'fix'], true)) { - fwrite(STDERR, "Usage: php dev.php cs (see: php dev.php help)\n"); + fwrite(STDERR, "Usage: php docdev.php cs (see: php docdev.php help)\n"); return 1; } } @@ -113,7 +113,7 @@ public function run(array $args): int : (new ShellCommand($environment))->execute($options); } - fwrite(STDERR, "Unknown command: $command (see: php dev.php help)\n"); + fwrite(STDERR, "Unknown command: $command (see: php docdev.php help)\n"); return 1; } diff --git a/scripts/dev/Command/HelpCommand.php b/scripts/dev/Command/HelpCommand.php index e70cba6000..d03acc15eb 100644 --- a/scripts/dev/Command/HelpCommand.php +++ b/scripts/dev/Command/HelpCommand.php @@ -17,7 +17,7 @@ public function execute(Options $options): int use. Usage: - php dev.php [options] [extra arguments] + php docdev.php [options] [extra arguments] Commands: pull Clone missing sibling repositories and update existing ones diff --git a/scripts/dev/Command/ServeCommand.php b/scripts/dev/Command/ServeCommand.php index 83b5a768a2..1498a888fb 100644 --- a/scripts/dev/Command/ServeCommand.php +++ b/scripts/dev/Command/ServeCommand.php @@ -22,7 +22,7 @@ public function execute(Options $options): int $output = $this->workspace->langDir($lang) . '/output'; if (!is_dir($output)) { - echo "Note: $output does not exist yet; run \"php dev.php render xhtml --lang=$lang\" first.\n"; + echo "Note: $output does not exist yet; run \"php docdev.php render xhtml --lang=$lang\" first.\n"; } // PhD renders each format into its own subdirectory of output/. From b293d744929fa78f8e5b25ab04a1002560eb5b80 Mon Sep 17 00:00:00 2001 From: Jordi Kroon Date: Tue, 11 Aug 2026 10:02:49 +0200 Subject: [PATCH 5/5] Rename dev.php to phpdoc.php --- phpdoc.php | 2 +- scripts/dev/Application.php | 8 ++++---- scripts/dev/Command/HelpCommand.php | 2 +- scripts/dev/Command/ServeCommand.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/phpdoc.php b/phpdoc.php index e53b6e9cb1..92d7a671db 100755 --- a/phpdoc.php +++ b/phpdoc.php @@ -3,7 +3,7 @@ /** * Dev build tool for the PHP manual, usable for every language - * Run "php docdev.php help" for usage + * Run "php phpdoc.php help" for usage */ declare(strict_types=1); diff --git a/scripts/dev/Application.php b/scripts/dev/Application.php index 368ca6dbed..cb73f39667 100644 --- a/scripts/dev/Application.php +++ b/scripts/dev/Application.php @@ -44,7 +44,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['build', 'shell'], true)) { - fwrite(STDERR, "Usage: php docdev.php docker (see: php docdev.php help)\n"); + fwrite(STDERR, "Usage: php phpdoc.php docker (see: php phpdoc.php help)\n"); return 1; } @@ -55,7 +55,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['xhtml', 'php'], true)) { - fwrite(STDERR, "Usage: php docdev.php render (see: php docdev.php help)\n"); + fwrite(STDERR, "Usage: php phpdoc.php render (see: php phpdoc.php help)\n"); return 1; } @@ -73,7 +73,7 @@ public function run(array $args): int $subcommand = array_shift($options->args); if (!in_array($subcommand, ['lint', 'fix'], true)) { - fwrite(STDERR, "Usage: php docdev.php cs (see: php docdev.php help)\n"); + fwrite(STDERR, "Usage: php phpdoc.php cs (see: php phpdoc.php help)\n"); return 1; } } @@ -113,7 +113,7 @@ public function run(array $args): int : (new ShellCommand($environment))->execute($options); } - fwrite(STDERR, "Unknown command: $command (see: php docdev.php help)\n"); + fwrite(STDERR, "Unknown command: $command (see: php phpdoc.php help)\n"); return 1; } diff --git a/scripts/dev/Command/HelpCommand.php b/scripts/dev/Command/HelpCommand.php index d03acc15eb..a5962b4500 100644 --- a/scripts/dev/Command/HelpCommand.php +++ b/scripts/dev/Command/HelpCommand.php @@ -17,7 +17,7 @@ public function execute(Options $options): int use. Usage: - php docdev.php [options] [extra arguments] + php phpdoc.php [options] [extra arguments] Commands: pull Clone missing sibling repositories and update existing ones diff --git a/scripts/dev/Command/ServeCommand.php b/scripts/dev/Command/ServeCommand.php index 1498a888fb..da0c03f112 100644 --- a/scripts/dev/Command/ServeCommand.php +++ b/scripts/dev/Command/ServeCommand.php @@ -22,7 +22,7 @@ public function execute(Options $options): int $output = $this->workspace->langDir($lang) . '/output'; if (!is_dir($output)) { - echo "Note: $output does not exist yet; run \"php docdev.php render xhtml --lang=$lang\" first.\n"; + echo "Note: $output does not exist yet; run \"php phpdoc.php render xhtml --lang=$lang\" first.\n"; } // PhD renders each format into its own subdirectory of output/.