From 2a61514f905fb4f0bab7f8b5bab8eb2dc7163f12 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 8 Jul 2025 12:57:55 +0400 Subject: [PATCH 01/38] feat: add Velox configuration into Software Registry --- resources/software.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/resources/software.json b/resources/software.json index 7cba844..a95149c 100644 --- a/resources/software.json +++ b/resources/software.json @@ -19,6 +19,22 @@ } ] }, + { + "name": "Velox", + "alias": "velox", + "binary": { + "name": "vx", + "version-command": "--version" + }, + "homepage": "https://roadrunner.dev", + "description": "Automated build system for the RoadRunner with custom plugins", + "repositories": [ + { + "type": "github", + "uri": "roadrunner-server/velox" + } + ] + }, { "name": "Temporal", "alias": "temporal", From 9b50867568567d7d2348bab18969f8474d9c57b1 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 8 Jul 2025 22:56:43 +0400 Subject: [PATCH 02/38] feat: add Velox build action and plugin configuration --- dload.xsd | 59 ++++++++++++++++++- src/Module/Config/Schema/Action/Velox.php | 46 +++++++++++++++ .../Config/Schema/Action/Velox/Plugin.php | 37 ++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/Module/Config/Schema/Action/Velox.php create mode 100644 src/Module/Config/Schema/Action/Velox/Plugin.php diff --git a/dload.xsd b/dload.xsd index 5a69122..ee4dd06 100644 --- a/dload.xsd +++ b/dload.xsd @@ -58,11 +58,68 @@ + + + + Velox build action for creating custom RoadRunner binaries + + + + + + Plugin to include in the RoadRunner build + + + + + Plugin name (required) + + + + + Plugin version constraint + + + + + Repository owner/organization + + + + + Repository name + + + + + + + + Version constraint for velox build tool + + + + + Required Go version constraint + + + + + RoadRunner version to display in --version + + + + + Path to local velox.toml file + + + + @@ -185,4 +242,4 @@ - \ No newline at end of file + diff --git a/src/Module/Config/Schema/Action/Velox.php b/src/Module/Config/Schema/Action/Velox.php new file mode 100644 index 0000000..0075af4 --- /dev/null +++ b/src/Module/Config/Schema/Action/Velox.php @@ -0,0 +1,46 @@ + + * 2. Remote API config: + * 3. Mixed approach: local base + additional plugins via API + * + * @internal + * @link https://docs.roadrunner.dev/docs/customization/build + */ +final class Velox +{ + /** @var non-empty-string|null $veloxVersion Version constraint for velox build tool */ + #[XPath('@velox-version')] + public ?string $veloxVersion = null; + + /** @var non-empty-string|null $golangVersion Required Go version constraint */ + #[XPath('@golang-version')] + public ?string $golangVersion = null; + + /** @var non-empty-string|null $binaryVersion RoadRunner version to display in --version */ + #[XPath('@binary-version')] + public ?string $binaryVersion = null; + + /** @var non-empty-string|null $configFile Path to local velox.toml file */ + #[XPath('@config-file')] + public ?string $configFile = null; + + /** @var list $plugins List of plugins to include in build */ + #[XPathEmbedList('plugin', Plugin::class)] + public array $plugins = []; +} diff --git a/src/Module/Config/Schema/Action/Velox/Plugin.php b/src/Module/Config/Schema/Action/Velox/Plugin.php new file mode 100644 index 0000000..a69f978 --- /dev/null +++ b/src/Module/Config/Schema/Action/Velox/Plugin.php @@ -0,0 +1,37 @@ + + * - With version: + * - Full specification: + * + * @internal + */ +final class Plugin +{ + /** @var non-empty-string $name Plugin name (required) */ + #[XPath('@name')] + public string $name; + + /** @var non-empty-string|null $version Plugin version constraint */ + #[XPath('@version')] + public ?string $version = null; + + /** @var non-empty-string|null $owner Repository owner/organization */ + #[XPath('@owner')] + public ?string $owner = null; + + /** @var non-empty-string|null $repository Repository name */ + #[XPath('@repository')] + public ?string $repository = null; +} From 6f7a944641870303424ae0a3d10fc4b1f6af43a6 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 8 Jul 2025 23:13:49 +0400 Subject: [PATCH 03/38] feat: add Velox API draft --- src/Module/Velox/ApiClient.php | 61 +++++++++++++++++ src/Module/Velox/Builder.php | 62 +++++++++++++++++ src/Module/Velox/Exception/Api.php | 21 ++++++ src/Module/Velox/Exception/Build.php | 20 ++++++ src/Module/Velox/Exception/Config.php | 20 ++++++ src/Module/Velox/Exception/Dependency.php | 20 ++++++ src/Module/Velox/Result.php | 80 ++++++++++++++++++++++ src/Module/Velox/Task.php | 83 +++++++++++++++++++++++ 8 files changed, 367 insertions(+) create mode 100644 src/Module/Velox/ApiClient.php create mode 100644 src/Module/Velox/Builder.php create mode 100644 src/Module/Velox/Exception/Api.php create mode 100644 src/Module/Velox/Exception/Build.php create mode 100644 src/Module/Velox/Exception/Config.php create mode 100644 src/Module/Velox/Exception/Dependency.php create mode 100644 src/Module/Velox/Result.php create mode 100644 src/Module/Velox/Task.php diff --git a/src/Module/Velox/ApiClient.php b/src/Module/Velox/ApiClient.php new file mode 100644 index 0000000..126d5eb --- /dev/null +++ b/src/Module/Velox/ApiClient.php @@ -0,0 +1,61 @@ + $plugins List of plugins to include + * @param string|null $golangVersion Go version constraint + * @param string|null $binaryVersion RoadRunner binary version + * @param array $options Additional configuration options + * @return string Generated velox.toml content + * @throws Exception\Api When API request fails + * @throws Exception\Config When generated config is invalid + */ + public function generateConfig( + array $plugins, + ?string $golangVersion = null, + ?string $binaryVersion = null, + array $options = [], + ): string; + + /** + * Validates plugin specifications against the API. + * + * @param list $plugins Plugins to validate + * @return array Validation results + * @throws Exception\Api When API request fails + */ + public function validatePlugins(array $plugins): array; + + /** + * Retrieves available plugin information from the API. + * + * @param string|null $search Optional search term + * @return array Available plugins + * @throws Exception\Api When API request fails + */ + public function getAvailablePlugins(?string $search = null): array; + + /** + * Checks API availability and health. + * + * @return bool True if API is available + */ + public function isAvailable(): bool; +} diff --git a/src/Module/Velox/Builder.php b/src/Module/Velox/Builder.php new file mode 100644 index 0000000..c7fc2a8 --- /dev/null +++ b/src/Module/Velox/Builder.php @@ -0,0 +1,62 @@ + $metadata Additional build metadata + * @param int $buildDuration Build time in seconds + * @param list $artifacts Additional artifacts created during build + */ + public function __construct( + public readonly Path $binaryPath, + public readonly Version $version, + public readonly array $metadata = [], + public readonly int $buildDuration = 0, + public readonly array $artifacts = [], + ) {} + + /** + * Checks if the built binary exists and is executable. + * + * @return bool True if binary is valid and executable + */ + public function isValid(): bool + { + return $this->binaryPath->exists() + && $this->binaryPath->isFile() + && \is_executable((string) $this->binaryPath); + } + + /** + * Returns the size of the built binary in bytes. + * + * @return int|null Binary size or null if file doesn't exist + */ + public function getBinarySize(): ?int + { + if (!$this->binaryPath->exists()) { + return null; + } + return \filesize((string) $this->binaryPath) ?: null; + } + + /** + * Returns build metadata as a formatted string. + * + * @return string Human-readable build information + */ + public function getSummary(): string + { + $size = $this->getBinarySize(); + $sizeStr = $size !== null ? \sprintf('%.2f MB', $size / 1024 / 1024) : 'unknown'; + + return \sprintf( + 'Built %s v%s (%s, %ds)', + $this->binaryPath->name(), + $this->version, + $sizeStr, + $this->buildDuration, + ); + } +} diff --git a/src/Module/Velox/Task.php b/src/Module/Velox/Task.php new file mode 100644 index 0000000..f65d16f --- /dev/null +++ b/src/Module/Velox/Task.php @@ -0,0 +1,83 @@ + $handler Build execution handler + * @param string $name Optional task name for identification + */ + public function __construct( + public readonly VeloxAction $config, + public readonly \Closure $onProgress, + public readonly \Closure $handler, + public readonly string $name = 'velox-build', + ) {} + + /** + * Executes the build task. + * + * @return PromiseInterface Promise that resolves to build result + */ + public function execute(): PromiseInterface + { + return ($this->handler)(); + } + + /** + * Reports progress to the registered callback. + * + * @param Progress $progress Current progress state + */ + public function reportProgress(Progress $progress): void + { + ($this->onProgress)($progress); + } + + /** + * Returns a unique identifier for this task. + * + * @return string Task identifier + */ + public function getId(): string + { + return \sprintf('%s-%s', $this->name, \spl_object_hash($this)); + } + + /** + * Returns task configuration summary. + * + * @return string Human-readable task description + */ + public function getDescription(): string + { + $pluginCount = \count($this->config->plugins); + $configSource = $this->config->configFile ? 'local config' : 'API config'; + + return \sprintf( + 'Build RoadRunner with %d plugins using %s', + $pluginCount, + $configSource, + ); + } +} From 323c7f41ef14ae1e335279450d4464d5551bc145 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 8 Jul 2025 23:22:32 +0400 Subject: [PATCH 04/38] feat: separate Task Manager into `Task` module --- src/DLoad.php | 4 ++-- src/Module/Config/Schema/Action/Velox.php | 4 ++++ src/Module/Config/Schema/Actions.php | 8 ++++++- src/Module/Downloader/Downloader.php | 1 + .../Downloader/Internal/DownloadContext.php | 2 +- src/Module/Downloader/Task/DownloadTask.php | 2 +- .../TaskManager.php => Task/Manager.php} | 4 ++-- src/Module/{Downloader => Task}/Progress.php | 2 +- src/Module/Velox/Builder.php | 23 ++++--------------- src/Module/Velox/Task.php | 2 +- 10 files changed, 25 insertions(+), 27 deletions(-) rename src/Module/{Downloader/TaskManager.php => Task/Manager.php} (97%) rename src/Module/{Downloader => Task}/Progress.php (93%) diff --git a/src/DLoad.php b/src/DLoad.php index c8fc892..3baf4f4 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -17,7 +17,7 @@ use Internal\DLoad\Module\Downloader\SoftwareCollection; use Internal\DLoad\Module\Downloader\Task\DownloadResult; use Internal\DLoad\Module\Downloader\Task\DownloadTask; -use Internal\DLoad\Module\Downloader\TaskManager; +use Internal\DLoad\Module\Task\Manager; use Internal\DLoad\Module\Version\Constraint; use Internal\DLoad\Module\Version\Version; use Internal\DLoad\Service\Logger; @@ -47,7 +47,7 @@ final class DLoad public function __construct( private readonly Logger $logger, - private readonly TaskManager $taskManager, + private readonly Manager $taskManager, private readonly SoftwareCollection $softwareCollection, private readonly Downloader $downloader, private readonly ArchiveFactory $archiveFactory, diff --git a/src/Module/Config/Schema/Action/Velox.php b/src/Module/Config/Schema/Action/Velox.php index 0075af4..041c3bb 100644 --- a/src/Module/Config/Schema/Action/Velox.php +++ b/src/Module/Config/Schema/Action/Velox.php @@ -43,4 +43,8 @@ final class Velox /** @var list $plugins List of plugins to include in build */ #[XPathEmbedList('plugin', Plugin::class)] public array $plugins = []; + + /** @var non-empty-string|null $binaryPath Path to the RoadRunner binary to build */ + #[XPath('@binary-path')] + public ?string $binaryPath = null; } diff --git a/src/Module/Config/Schema/Actions.php b/src/Module/Config/Schema/Actions.php index 7290da9..3c72b3b 100644 --- a/src/Module/Config/Schema/Actions.php +++ b/src/Module/Config/Schema/Actions.php @@ -6,11 +6,13 @@ use Internal\DLoad\Module\Common\Internal\Attribute\XPathEmbedList; use Internal\DLoad\Module\Config\Schema\Action\Download; +use Internal\DLoad\Module\Config\Schema\Action\Velox; /** * Configuration actions container. * - * Contains the list of download actions defined in the configuration file. + * Contains the list of actions defined in the configuration file, + * including both download and build actions. * * @internal */ @@ -19,4 +21,8 @@ final class Actions /** @var list $downloads Collection of download actions */ #[XPathEmbedList('/dload/actions/download', Download::class)] public array $downloads = []; + + /** @var list $veloxBuilds Collection of velox build actions */ + #[XPathEmbedList('/dload/actions/velox', Velox::class)] + public array $veloxBuilds = []; } diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index 4d304c7..5d94cd6 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -23,6 +23,7 @@ use Internal\DLoad\Module\Repository\ReleaseInterface; use Internal\DLoad\Module\Repository\Repository; use Internal\DLoad\Module\Repository\RepositoryProvider; +use Internal\DLoad\Module\Task\Progress; use Internal\DLoad\Module\Version\Constraint; use Internal\DLoad\Service\Destroyable; use Internal\DLoad\Service\Logger; diff --git a/src/Module/Downloader/Internal/DownloadContext.php b/src/Module/Downloader/Internal/DownloadContext.php index 5ea76e1..eaa3576 100644 --- a/src/Module/Downloader/Internal/DownloadContext.php +++ b/src/Module/Downloader/Internal/DownloadContext.php @@ -8,9 +8,9 @@ use Internal\DLoad\Module\Config\Schema\Action\Download as DownloadConfig; use Internal\DLoad\Module\Config\Schema\Embed\Repository; use Internal\DLoad\Module\Config\Schema\Embed\Software; -use Internal\DLoad\Module\Downloader\Progress; use Internal\DLoad\Module\Repository\AssetInterface; use Internal\DLoad\Module\Repository\ReleaseInterface; +use Internal\DLoad\Module\Task\Progress; /** * Context object for download operations. diff --git a/src/Module/Downloader/Task/DownloadTask.php b/src/Module/Downloader/Task/DownloadTask.php index 433e5c7..eac10a6 100644 --- a/src/Module/Downloader/Task/DownloadTask.php +++ b/src/Module/Downloader/Task/DownloadTask.php @@ -5,7 +5,7 @@ namespace Internal\DLoad\Module\Downloader\Task; use Internal\DLoad\Module\Config\Schema\Embed\Software; -use Internal\DLoad\Module\Downloader\Progress; +use Internal\DLoad\Module\Task\Progress; use React\Promise\PromiseInterface; /** diff --git a/src/Module/Downloader/TaskManager.php b/src/Module/Task/Manager.php similarity index 97% rename from src/Module/Downloader/TaskManager.php rename to src/Module/Task/Manager.php index 1fc8bc7..5768afa 100644 --- a/src/Module/Downloader/TaskManager.php +++ b/src/Module/Task/Manager.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Downloader; +namespace Internal\DLoad\Module\Task; use Internal\DLoad\Service\Logger; @@ -27,7 +27,7 @@ * $taskManager->await(); * ``` */ -final class TaskManager +final class Manager { /** @var array<\Fiber> Active fiber tasks */ private array $tasks = []; diff --git a/src/Module/Downloader/Progress.php b/src/Module/Task/Progress.php similarity index 93% rename from src/Module/Downloader/Progress.php rename to src/Module/Task/Progress.php index 1d9bb8b..89df577 100644 --- a/src/Module/Downloader/Progress.php +++ b/src/Module/Task/Progress.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Downloader; +namespace Internal\DLoad\Module\Task; /** * Represents download progress information. diff --git a/src/Module/Velox/Builder.php b/src/Module/Velox/Builder.php index c7fc2a8..2b46318 100644 --- a/src/Module/Velox/Builder.php +++ b/src/Module/Velox/Builder.php @@ -6,22 +6,21 @@ use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; -use Internal\DLoad\Module\Downloader\Progress; +use Internal\DLoad\Module\Task\Progress; /** * Builder interface for creating custom software builds. * - * Provides a contract for building software from source with custom configurations. - * Implementations handle dependency management, configuration generation, and build execution. + * Provides a contract for building software from a Velox configuration. * * @internal */ interface Builder { /** - * Builds software according to the provided configuration. + * Creates a task to build software from the provided configuration. * - * Executes the complete build workflow: + * The task executes the complete build workflow: * 1. Downloads and verifies dependencies * 2. Generates or processes configuration files * 3. Executes the build process @@ -31,12 +30,8 @@ interface Builder * @param VeloxAction $config Build configuration * @param Path $destination Target directory for the built binary * @param \Closure(Progress): mixed $onProgress Progress callback - * @return Result Build result with binary information - * @throws Exception\Build When build process fails - * @throws Exception\Dependency When dependencies cannot be resolved - * @throws Exception\Config When configuration is invalid */ - public function build(VeloxAction $config, Path $destination, \Closure $onProgress): Result; + public function build(VeloxAction $config, Path $destination, \Closure $onProgress): Task; /** * Validates build configuration without executing the build. @@ -51,12 +46,4 @@ public function build(VeloxAction $config, Path $destination, \Closure $onProgre * @throws Exception\Dependency When dependencies are unavailable */ public function validate(VeloxAction $config): void; - - /** - * Estimates build time based on configuration complexity. - * - * @param VeloxAction $config Build configuration - * @return int Estimated build time in seconds - */ - public function estimateBuildTime(VeloxAction $config): int; } diff --git a/src/Module/Velox/Task.php b/src/Module/Velox/Task.php index f65d16f..0777e13 100644 --- a/src/Module/Velox/Task.php +++ b/src/Module/Velox/Task.php @@ -5,7 +5,7 @@ namespace Internal\DLoad\Module\Velox; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; -use Internal\DLoad\Module\Downloader\Progress; +use Internal\DLoad\Module\Task\Progress; use React\Promise\PromiseInterface; /** From 240219d21086d9399b5cb4216ae903d7505fc095 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 9 Jul 2025 00:11:12 +0400 Subject: [PATCH 05/38] feat(Container): enhance binding method to support class name aliases --- src/Bootstrap.php | 10 ++-------- src/Module/Common/Internal/ObjectContainer.php | 15 +++++++++++++-- src/Service/Container.php | 5 +++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Bootstrap.php b/src/Bootstrap.php index a3a80ef..07e911a 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -104,14 +104,8 @@ public function withConfig( static fn(Container $container): RepositoryProvider => (new RepositoryProvider()) ->addRepositoryFactory($container->get(GithubRepositoryFactory::class)), ); - $this->container->bind( - BinaryProvider::class, - static fn(Container $c): BinaryProvider => $c->get(BinaryProviderImpl::class), - ); - $this->container->bind( - Factory::class, - static fn(Container $c): Factory => $c->get(NyholmFactoryImpl::class), - ); + $this->container->bind(BinaryProvider::class, BinaryProviderImpl::class); + $this->container->bind(Factory::class, NyholmFactoryImpl::class); return $this; } diff --git a/src/Module/Common/Internal/ObjectContainer.php b/src/Module/Common/Internal/ObjectContainer.php index eeac057..1db6ee3 100644 --- a/src/Module/Common/Internal/ObjectContainer.php +++ b/src/Module/Common/Internal/ObjectContainer.php @@ -89,10 +89,21 @@ public function make(string $class, array $arguments = []): object /** * @template T * @param class-string $id Service identifier - * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments + * @param null|class-string|array|\Closure(Container): T $binding */ - public function bind(string $id, \Closure|array|null $binding = null): void + public function bind(string $id, \Closure|string|array|null $binding = null): void { + if (\is_string($binding)) { + \class_exists($binding) or throw new \InvalidArgumentException( + "Class `$binding` does not exist.", + ); + + /** @var class-string $binding */ + $binding = \is_a($binding, Factoriable::class, true) + ? fn(): object => $this->injector->invoke([$binding, 'create']) + : fn(): object => $this->injector->make($binding); + } + if ($binding !== null) { $this->factory[$id] = $binding; return; diff --git a/src/Service/Container.php b/src/Service/Container.php index f39b318..24dba6e 100644 --- a/src/Service/Container.php +++ b/src/Service/Container.php @@ -77,7 +77,8 @@ public function make(string $class, array $arguments = []): object; * * @template T * @param class-string $id Service identifier - * @param null|array|\Closure(Container): T $binding Factory function or constructor arguments + * @param null|class-string|array|\Closure(Container): T $binding Factory + * function, constructor arguments, or alias class name. */ - public function bind(string $id, \Closure|array|null $binding = null): void; + public function bind(string $id, \Closure|string|array|null $binding = null): void; } From c8f4d8618817bf286a74f22f4dab7d8e451fc6af Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 9 Jul 2025 22:18:42 +0400 Subject: [PATCH 06/38] feat(Binary): implement GlobalBinary for system binaries and rename BinaryHandle to LocalBinary --- src/Command/Show.php | 9 +- src/DLoad.php | 2 +- src/Module/Binary/BinaryProvider.php | 16 +- .../Binary/Internal/BinaryProviderImpl.php | 18 ++- src/Module/Binary/Internal/GlobalBinary.php | 151 ++++++++++++++++++ .../{BinaryHandle.php => LocalBinary.php} | 2 +- 6 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 src/Module/Binary/Internal/GlobalBinary.php rename src/Module/Binary/Internal/{BinaryHandle.php => LocalBinary.php} (98%) diff --git a/src/Command/Show.php b/src/Command/Show.php index 4e4829f..3e9bf93 100644 --- a/src/Command/Show.php +++ b/src/Command/Show.php @@ -103,7 +103,7 @@ private function listAllSoftware( continue; } - $binary = $binaryProvider->getBinary($destinationPath, $software->binary); + $binary = $binaryProvider->getLocalBinary($destinationPath, $software->binary, $software->name); if ($binary === null) { continue; } @@ -146,7 +146,7 @@ private function listAllSoftware( continue; } - $binary = $binaryProvider->getBinary($destinationPath, $software->binary); + $binary = $binaryProvider->getLocalBinary($destinationPath, $software->binary, $software->name); if ($binary === null) { continue; } @@ -224,8 +224,7 @@ private function showSoftwareDetails( return Command::FAILURE; } - $destinationPath = \getcwd(); - + $destinationPath = Path::create(\getcwd() ?: '.'); // Check if software is in project config $inConfig = false; @@ -278,7 +277,7 @@ private function showSoftwareDetails( } // Binary information - $binary = $binaryProvider->getBinary($destinationPath, $software->binary); + $binary = $binaryProvider->getLocalBinary($destinationPath, $software->binary, $software->name); $this->displayBinaryDetails($binary, $output); diff --git a/src/DLoad.php b/src/DLoad.php index 3baf4f4..2dd7947 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -80,7 +80,7 @@ public function addTask(DownloadConfig $action, bool $force = false): void if (!$force && ($type === null || $type === Type::Binary) && $software->binary !== null) { // Check different constraints - $binary = $this->binaryProvider->getBinary($destinationPath, $software->binary); + $binary = $this->binaryProvider->getLocalBinary($destinationPath, $software->binary, $software->name); if ($binary === null) { goto add_task; diff --git a/src/Module/Binary/BinaryProvider.php b/src/Module/Binary/BinaryProvider.php index 4850036..35fd975 100644 --- a/src/Module/Binary/BinaryProvider.php +++ b/src/Module/Binary/BinaryProvider.php @@ -17,7 +17,21 @@ interface BinaryProvider * * @param Path|non-empty-string $destinationPath Directory path where binary should exist * @param BinaryConfig $config Binary configuration + * @param non-empty-string|null $name Software name to use in the binary instance. + * It's not required to match the binary name. + * + * @return Binary|null Binary instance or null if it doesn't exist + */ + public function getLocalBinary(Path|string $destinationPath, BinaryConfig $config, ?string $name = null): ?Binary; + + /** + * Gets a globally available binary by its configuration. + * + * @param BinaryConfig $config Binary configuration + * @param non-empty-string|null $name Software name to use in the binary instance. + * It's not required to match the binary name. + * * @return Binary|null Binary instance or null if it doesn't exist */ - public function getBinary(Path|string $destinationPath, BinaryConfig $config): ?Binary; + public function getGlobalBinary(BinaryConfig $config, ?string $name = null): ?Binary; } diff --git a/src/Module/Binary/Internal/BinaryProviderImpl.php b/src/Module/Binary/Internal/BinaryProviderImpl.php index 1c1398f..d8a64c9 100644 --- a/src/Module/Binary/Internal/BinaryProviderImpl.php +++ b/src/Module/Binary/Internal/BinaryProviderImpl.php @@ -22,14 +22,14 @@ public function __construct( private readonly BinaryExecutor $executor, ) {} - public function getBinary(Path|string $destinationPath, BinaryConfig $config): ?Binary + public function getLocalBinary(Path|string $destinationPath, BinaryConfig $config, ?string $name = null): ?Binary { // Get binary path $binaryPath = $this->buildBinaryPath($destinationPath, $config); // Create binary instance - $binary = new BinaryHandle( - name: $config->name, + $binary = new LocalBinary( + name: $name ?? $config->name, path: $binaryPath, config: $config, executor: $this->executor, @@ -39,6 +39,18 @@ public function getBinary(Path|string $destinationPath, BinaryConfig $config): ? return $binary->exists() ? $binary : null; } + public function getGlobalBinary(BinaryConfig $config, ?string $name = null): ?Binary + { + $binary = new GlobalBinary( + name: $name ?? $config->name, + config: $config, + executor: $this->executor, + ); + + // Return binary only if it exists + return $binary->exists() ? $binary : null; + } + /** * Builds the path to a binary without checking if it exists. * diff --git a/src/Module/Binary/Internal/GlobalBinary.php b/src/Module/Binary/Internal/GlobalBinary.php new file mode 100644 index 0000000..ce3c88d --- /dev/null +++ b/src/Module/Binary/Internal/GlobalBinary.php @@ -0,0 +1,151 @@ +name; + } + + public function getPath(): Path + { + $this->resolvePath(); + return $this->resolvedPath ?? throw new \RuntimeException("Can't resolve path for binary `{$this->name}`"); + } + + /** + * @psalm-assert-if-true !null $this->resolvedPath + */ + public function exists(): bool + { + $this->resolvePath(); + return $this->resolvedPath !== null && $this->resolvedPath->exists(); + } + + public function getVersion(): ?BinaryVersion + { + if ($this->versionOutput !== null) { + return $this->versionOutput; + } + + if (!$this->exists() || $this->config->versionCommand === null) { + return null; + } + + try { + $output = $this->executor->execute($this->resolvedPath, $this->config->versionCommand); + return $this->versionOutput = BinaryVersion::fromBinaryOutput($output); + } catch (\Throwable) { + return $this->versionOutput = BinaryVersion::empty(); + } + } + + public function getVersionString(): ?string + { + return $this->getVersion()?->number; + } + + public function getSize(): ?int + { + if (!$this->exists()) { + return null; + } + + $size = \filesize((string) $this->resolvedPath); + return $size === false ? null : $size; + } + + public function getMTime(): ?\DateTimeImmutable + { + if (!$this->exists()) { + return null; + } + + $mtime = \filemtime((string) $this->resolvedPath); + if ($mtime === false) { + return null; + } + + try { + return new \DateTimeImmutable('@' . $mtime); + } catch (\Exception) { + return null; + } + } + + /** + * Resolves the binary path from system PATH environment variable. + */ + private function resolvePath(): void + { + if ($this->resolvedPath !== null) { + return; + } + + try { + $binaryPath = $this->findBinaryInPath($this->name); + $this->resolvedPath = $binaryPath !== null ? Path::create($binaryPath) : null; + } catch (\Throwable) { + $this->resolvedPath = null; + } + } + + /** + * Finds binary executable in system PATH. + * + * @param non-empty-string $binaryName Binary name to find + * @return non-empty-string|null Full path to binary or null if not found + */ + private function findBinaryInPath(string $binaryName): ?string + { + $isWindows = \PHP_OS_FAMILY === 'Windows'; + $command = $isWindows ? 'where' : 'which'; + + // Escape binary name for shell execution + $escapedBinaryName = \escapeshellarg($binaryName); + + // Execute command to find binary + $output = []; + $returnCode = 0; + + \exec("$command $escapedBinaryName", $output, $returnCode); + + if ($returnCode !== 0 || $output === []) { + return null; + } + + // Return first found path (most relevant) + $binaryPath = \trim($output[0]); + return $binaryPath !== '' ? $binaryPath : null; + } +} diff --git a/src/Module/Binary/Internal/BinaryHandle.php b/src/Module/Binary/Internal/LocalBinary.php similarity index 98% rename from src/Module/Binary/Internal/BinaryHandle.php rename to src/Module/Binary/Internal/LocalBinary.php index 86e5b7e..1179583 100644 --- a/src/Module/Binary/Internal/BinaryHandle.php +++ b/src/Module/Binary/Internal/LocalBinary.php @@ -14,7 +14,7 @@ * * @internal */ -final class BinaryHandle implements Binary +final class LocalBinary implements Binary { private ?BinaryVersion $versionOutput = null; From aca1c774c8090a94ad4ab297c3f4c2d4a2b05a95 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 21:40:54 +0400 Subject: [PATCH 07/38] feat(FileSystem): add FS utility class for file system operations --- src/Module/Common/FileSystem/FS.php | 43 ++++++++++++++++++++++++++++ src/Module/Downloader/Downloader.php | 3 +- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/Module/Common/FileSystem/FS.php diff --git a/src/Module/Common/FileSystem/FS.php b/src/Module/Common/FileSystem/FS.php new file mode 100644 index 0000000..db76847 --- /dev/null +++ b/src/Module/Common/FileSystem/FS.php @@ -0,0 +1,43 @@ +exists() or self::mkdir((string) $result); + + return $result; + } +} diff --git a/src/Module/Downloader/Downloader.php b/src/Module/Downloader/Downloader.php index 5d94cd6..0858f33 100644 --- a/src/Module/Downloader/Downloader.php +++ b/src/Module/Downloader/Downloader.php @@ -6,6 +6,7 @@ use Internal\DLoad\Module\Archive\ArchiveFactory; use Internal\DLoad\Module\Common\Architecture; +use Internal\DLoad\Module\Common\FileSystem\FS; use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Common\Stability; @@ -419,7 +420,7 @@ private function processAsset(DownloadContext $context): \Closure */ private function getTempDirectory(): Path { - $temp = Path::create($this->config->tmpDir ?? \sys_get_temp_dir()); + $temp = FS::tmpDir($this->config->tmpDir); $temp->exists() or \mkdir((string) $temp, recursive: true); $temp->isDir() && $temp->isWriteable() or throw new \LogicException( From c1c0e17744d06bc3227043830e407dbfa888257e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 22:16:59 +0400 Subject: [PATCH 08/38] feat(DependencyChecker): add dependency checker for Velox build requirements --- src/Module/Config/Schema/Downloader.php | 2 +- .../Velox/Internal/DependencyChecker.php | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/Module/Velox/Internal/DependencyChecker.php diff --git a/src/Module/Config/Schema/Downloader.php b/src/Module/Config/Schema/Downloader.php index b18f7d3..220473e 100644 --- a/src/Module/Config/Schema/Downloader.php +++ b/src/Module/Config/Schema/Downloader.php @@ -13,7 +13,7 @@ */ final class Downloader { - /** @var string|null $tmpDir Temporary directory for downloads */ + /** @var non-empty-string|null $tmpDir Temporary directory for downloads */ #[XPath('/dload/@temp-dir')] public ?string $tmpDir = null; } diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php new file mode 100644 index 0000000..440190c --- /dev/null +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -0,0 +1,157 @@ +checkServiceState(); + + # Prepare config + $binaryConfig = new BinaryConfig(); + $binaryConfig->name = self::GOLANG_BINARY_NAME; + $binaryConfig->versionCommand = 'version'; + + # Find Golang binary + $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Go') ?? throw new DependencyException( + 'Go (golang) binary not found. Please install Go or ensure it is in your PATH.', + dependencyName: self::GOLANG_BINARY_NAME, + ); + + $this->logger->debug('Found Go binary: %s', (string) $binary->getPath()); + + return $binary; + } + + /** + * Checks if Velox is available. + * + * @throws DependencyException When Velox is not found + */ + public function prepareVelox(): Binary + { + $this->checkServiceState(); + + # Prepare config + $binaryConfig = new BinaryConfig(); + $binaryConfig->name = self::VELOX_BINARY_NAME; + $binaryConfig->versionCommand = '--version'; + + # Check Velox globally + $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Velox'); + if ($binary !== null && $this->checkVeloxVersion($binary)) { + $this->logger->debug('Found global Velox binary: %s', (string) $binary->getPath()); + return $binary; + } + + # Check Velox locally + $binary = $this->binaryProvider->getLocalBinary( + $this->veloxPath, + $binaryConfig, + 'Velox', + ); + if ($binary !== null && $this->checkVeloxVersion($binary)) { + $this->logger->debug('Found local Velox binary: %s', (string) $binary->getPath()); + + return $binary; + } + + # todo: download Velox if not installed (execute Download actions) + # todo: check download actions + + # Throw exception if Velox is not found + $this->logger->error('Velox binary not found in PATH or local directory `%s`', (string) $this->veloxPath); + throw new DependencyException( + 'Velox binary not found. Please install Velox or ensure it is in your PATH.', + dependencyName: self::VELOX_BINARY_NAME, + ); + } + + /** + * Sets the Velox configuration for dependency checks. + * + * @param VeloxConfig $config The Velox configuration to use + * @param Path $buildDirectory The directory where the build will take place + * + * @return self A new instance with the updated configuration + */ + public function withConfig(VeloxConfig $config, Path $buildDirectory): self + { + $self = clone $this; + $self->config = $config; + $self->veloxPath = Path::create('.'); + $self->buildDirectory = $buildDirectory; + return $self; + } + + /** + * Checks if the Velox version satisfies the configured constraint. + * + * @param Binary $binary The Velox binary to check + * + * @return bool True if the version is satisfied, false otherwise + */ + private function checkVeloxVersion(Binary $binary): bool + { + if ($this->config->veloxVersion === null) { + return true; + } + + $version = $binary->getVersion(); + $constrain = Constraint::fromConstraintString($this->config->veloxVersion); + + return $version !== null && $constrain->isSatisfiedBy($version); + } + + /** + * Checks if the Velox service is properly configured. + * + * @throws \LogicException If the configuration is not set + */ + private function checkServiceState(): void + { + /** @psalm-suppress RedundantPropertyInitializationCheck */ + isset($this->config) or throw new \LogicException( + 'Velox configuration is not set. Use `withConfig()` to set it before checking dependencies.', + ); + } +} From 1ab12748ba2f2696adc930afcdbe022c39f65654 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 22:58:28 +0400 Subject: [PATCH 09/38] feat(Binary): refactor binary classes to use `AbstractBinary` and add `Binary::execute()` method --- src/Module/Binary/Binary.php | 8 ++ src/Module/Binary/Internal/AbstractBinary.php | 96 +++++++++++++++++++ .../Binary/Internal/BinaryProviderImpl.php | 2 +- src/Module/Binary/Internal/GlobalBinary.php | 78 ++------------- src/Module/Binary/Internal/LocalBinary.php | 75 ++------------- src/Module/Common/FileSystem/FS.php | 5 +- 6 files changed, 121 insertions(+), 143 deletions(-) create mode 100644 src/Module/Binary/Internal/AbstractBinary.php diff --git a/src/Module/Binary/Binary.php b/src/Module/Binary/Binary.php index dff2afa..e8515fb 100644 --- a/src/Module/Binary/Binary.php +++ b/src/Module/Binary/Binary.php @@ -52,4 +52,12 @@ public function getSize(): ?int; * @return \DateTimeImmutable|null Modification time or null if the binary doesn't exist */ public function getMTime(): ?\DateTimeImmutable; + + /** + * Executes the binary with the given string input. + * + * @param non-empty-string $args Arguments to pass to the binary + * @return string Output from the binary execution + */ + public function execute(string ...$args): string; } diff --git a/src/Module/Binary/Internal/AbstractBinary.php b/src/Module/Binary/Internal/AbstractBinary.php new file mode 100644 index 0000000..bff047b --- /dev/null +++ b/src/Module/Binary/Internal/AbstractBinary.php @@ -0,0 +1,96 @@ +name; + } + + public function execute(string ...$args): string + { + $args = \array_map(static fn(string $arg): string => \escapeshellarg($arg), $args); + + return $this->executor->execute($this->getPath(), \implode(' ', $args)); + } + + public function exists(): bool + { + return $this->getPath()->exists(); + } + + public function getSize(): ?int + { + if (!$this->exists()) { + return null; + } + + $size = \filesize((string) $this->getPath()); + return $size === false ? null : $size; + } + + public function getMTime(): ?\DateTimeImmutable + { + if (!$this->exists()) { + return null; + } + + $mtime = \filemtime((string) $this->getPath()); + if ($mtime === false) { + return null; + } + + try { + return new \DateTimeImmutable('@' . $mtime); + } catch (\Exception) { + return null; + } + } + + public function getVersion(): ?BinaryVersion + { + if ($this->versionOutput !== null) { + return $this->versionOutput; + } + + if (!$this->exists() || $this->config->versionCommand === null) { + return null; + } + + try { + $output = $this->executor->execute($this->getPath(), $this->config->versionCommand); + return $this->versionOutput = BinaryVersion::fromBinaryOutput($output); + } catch (\Throwable) { + return $this->versionOutput = BinaryVersion::empty(); + } + } + + public function getVersionString(): ?string + { + return $this->getVersion()?->number; + } + + abstract public function getPath(): Path; +} diff --git a/src/Module/Binary/Internal/BinaryProviderImpl.php b/src/Module/Binary/Internal/BinaryProviderImpl.php index d8a64c9..39a613d 100644 --- a/src/Module/Binary/Internal/BinaryProviderImpl.php +++ b/src/Module/Binary/Internal/BinaryProviderImpl.php @@ -30,9 +30,9 @@ public function getLocalBinary(Path|string $destinationPath, BinaryConfig $confi // Create binary instance $binary = new LocalBinary( name: $name ?? $config->name, - path: $binaryPath, config: $config, executor: $this->executor, + path: $binaryPath, ); // Return binary only if it exists diff --git a/src/Module/Binary/Internal/GlobalBinary.php b/src/Module/Binary/Internal/GlobalBinary.php index ce3c88d..6660c6a 100644 --- a/src/Module/Binary/Internal/GlobalBinary.php +++ b/src/Module/Binary/Internal/GlobalBinary.php @@ -4,8 +4,6 @@ namespace Internal\DLoad\Module\Binary\Internal; -use Internal\DLoad\Module\Binary\Binary; -use Internal\DLoad\Module\Binary\BinaryVersion; use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; @@ -16,10 +14,9 @@ * * @internal */ -final class GlobalBinary implements Binary +final class GlobalBinary extends AbstractBinary { private ?Path $resolvedPath = null; - private ?BinaryVersion $versionOutput = null; /** * @param non-empty-string $name Binary name to resolve from PATH @@ -27,14 +24,11 @@ final class GlobalBinary implements Binary * @param BinaryExecutor $executor Binary execution service */ public function __construct( - private readonly string $name, - private readonly BinaryConfig $config, - private readonly BinaryExecutor $executor, - ) {} - - public function getName(): string - { - return $this->name; + string $name, + BinaryConfig $config, + BinaryExecutor $executor, + ) { + parent::__construct($name, $config, $executor); } public function getPath(): Path @@ -43,66 +37,6 @@ public function getPath(): Path return $this->resolvedPath ?? throw new \RuntimeException("Can't resolve path for binary `{$this->name}`"); } - /** - * @psalm-assert-if-true !null $this->resolvedPath - */ - public function exists(): bool - { - $this->resolvePath(); - return $this->resolvedPath !== null && $this->resolvedPath->exists(); - } - - public function getVersion(): ?BinaryVersion - { - if ($this->versionOutput !== null) { - return $this->versionOutput; - } - - if (!$this->exists() || $this->config->versionCommand === null) { - return null; - } - - try { - $output = $this->executor->execute($this->resolvedPath, $this->config->versionCommand); - return $this->versionOutput = BinaryVersion::fromBinaryOutput($output); - } catch (\Throwable) { - return $this->versionOutput = BinaryVersion::empty(); - } - } - - public function getVersionString(): ?string - { - return $this->getVersion()?->number; - } - - public function getSize(): ?int - { - if (!$this->exists()) { - return null; - } - - $size = \filesize((string) $this->resolvedPath); - return $size === false ? null : $size; - } - - public function getMTime(): ?\DateTimeImmutable - { - if (!$this->exists()) { - return null; - } - - $mtime = \filemtime((string) $this->resolvedPath); - if ($mtime === false) { - return null; - } - - try { - return new \DateTimeImmutable('@' . $mtime); - } catch (\Exception) { - return null; - } - } - /** * Resolves the binary path from system PATH environment variable. */ diff --git a/src/Module/Binary/Internal/LocalBinary.php b/src/Module/Binary/Internal/LocalBinary.php index 1179583..0ff6698 100644 --- a/src/Module/Binary/Internal/LocalBinary.php +++ b/src/Module/Binary/Internal/LocalBinary.php @@ -4,8 +4,6 @@ namespace Internal\DLoad\Module\Binary\Internal; -use Internal\DLoad\Module\Binary\Binary; -use Internal\DLoad\Module\Binary\BinaryVersion; use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; @@ -14,10 +12,8 @@ * * @internal */ -final class LocalBinary implements Binary +final class LocalBinary extends AbstractBinary { - private ?BinaryVersion $versionOutput = null; - /** * @param non-empty-string $name Binary name * @param Path $path Path to binary @@ -25,75 +21,16 @@ final class LocalBinary implements Binary * @param BinaryExecutor $executor Binary execution service */ public function __construct( - private readonly string $name, + string $name, + BinaryConfig $config, + BinaryExecutor $executor, private readonly Path $path, - private readonly BinaryConfig $config, - private readonly BinaryExecutor $executor, - ) {} - - public function getName(): string - { - return $this->name; + ) { + parent::__construct($name, $config, $executor); } public function getPath(): Path { return $this->path; } - - public function exists(): bool - { - return $this->path->exists(); - } - - public function getVersion(): ?BinaryVersion - { - if ($this->versionOutput !== null) { - return $this->versionOutput; - } - - if (!$this->exists() || $this->config->versionCommand === null) { - return null; - } - - try { - $output = $this->executor->execute($this->path, $this->config->versionCommand); - return $this->versionOutput = BinaryVersion::fromBinaryOutput($output); - } catch (\Throwable) { - return $this->versionOutput = BinaryVersion::empty(); - } - } - - public function getVersionString(): ?string - { - return $this->getVersion()?->number; - } - - public function getSize(): ?int - { - if (!$this->exists()) { - return null; - } - - $size = \filesize((string) $this->path); - return $size === false ? null : $size; - } - - public function getMTime(): ?\DateTimeImmutable - { - if (!$this->exists()) { - return null; - } - - $mtime = \filemtime((string) $this->path); - if ($mtime === false) { - return null; - } - - try { - return new \DateTimeImmutable('@' . $mtime); - } catch (\Exception) { - return null; - } - } } diff --git a/src/Module/Common/FileSystem/FS.php b/src/Module/Common/FileSystem/FS.php index db76847..0e6f124 100644 --- a/src/Module/Common/FileSystem/FS.php +++ b/src/Module/Common/FileSystem/FS.php @@ -31,11 +31,14 @@ public static function mkdir(string $path, int $mode = 0777, bool $recursive = t * Creates a temporary directory. * * @param non-empty-string|null $path Path to the temporary directory. If null, uses system temp directory. + * @param non-empty-string|null $sub Optional subdirectory name to create within the temp directory. + * * @return Path The created temporary directory path. */ - public static function tmpDir(?string $path): Path + public static function tmpDir(?string $path = null, ?string $sub = null): Path { $result = Path::create($path ?? \sys_get_temp_dir()); + $sub === null or $result = $result->join($sub); $result->exists() or self::mkdir((string) $result); return $result; From 48deb507884a3eb7e31ee8f5f5a235e144f6b344 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 23:06:25 +0400 Subject: [PATCH 10/38] feat(BinaryExecutor, DependencyChecker): add logging for command execution and enhance version checks --- src/Module/Binary/Internal/BinaryExecutor.php | 10 ++++++- .../Velox/Internal/DependencyChecker.php | 26 ++++++++++++++----- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Module/Binary/Internal/BinaryExecutor.php b/src/Module/Binary/Internal/BinaryExecutor.php index 5a37bb2..31ff34c 100644 --- a/src/Module/Binary/Internal/BinaryExecutor.php +++ b/src/Module/Binary/Internal/BinaryExecutor.php @@ -6,6 +6,7 @@ use Internal\DLoad\Module\Binary\Exception\BinaryExecutionException; use Internal\DLoad\Module\Common\FileSystem\Path; +use Internal\DLoad\Service\Logger; /** * Executes binary commands and captures their output. @@ -14,6 +15,10 @@ */ final class BinaryExecutor { + public function __construct( + private readonly Logger $logger, + ) {} + /** * Executes a binary with the specified command and returns the output. * @@ -31,8 +36,11 @@ public function execute(Path $binaryPath, string $command): string $output = []; $returnCode = 0; + $cmd = "$escapedPath $command 2>&1"; + $this->logger->debug('Executing command: %s', $cmd); + // Execute with both stdout and stderr redirected to output - \exec("$escapedPath $command 2>&1", $output, $returnCode); + \exec($cmd, $output, $returnCode); // If command failed, throw exception if ($returnCode !== 0) { diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php index 440190c..fcb02f6 100644 --- a/src/Module/Velox/Internal/DependencyChecker.php +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -57,6 +57,18 @@ public function prepareGolang(): Binary dependencyName: self::GOLANG_BINARY_NAME, ); + # Check Go version + if (!$this->checkBinaryVersion($binary, $this->config->golangVersion)) { + throw new DependencyException( + \sprintf( + 'Go binary version `%s` does not satisfy the required constraint `%s`', + (string) $binary->getVersion(), + (string) $this->config->golangVersion, + ), + dependencyName: self::GOLANG_BINARY_NAME, + ); + } + $this->logger->debug('Found Go binary: %s', (string) $binary->getPath()); return $binary; @@ -78,7 +90,7 @@ public function prepareVelox(): Binary # Check Velox globally $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Velox'); - if ($binary !== null && $this->checkVeloxVersion($binary)) { + if ($binary !== null && $this->checkBinaryVersion($binary, $this->config->veloxVersion)) { $this->logger->debug('Found global Velox binary: %s', (string) $binary->getPath()); return $binary; } @@ -89,7 +101,7 @@ public function prepareVelox(): Binary $binaryConfig, 'Velox', ); - if ($binary !== null && $this->checkVeloxVersion($binary)) { + if ($binary !== null && $this->checkBinaryVersion($binary, $this->config->veloxVersion)) { $this->logger->debug('Found local Velox binary: %s', (string) $binary->getPath()); return $binary; @@ -99,7 +111,6 @@ public function prepareVelox(): Binary # todo: check download actions # Throw exception if Velox is not found - $this->logger->error('Velox binary not found in PATH or local directory `%s`', (string) $this->veloxPath); throw new DependencyException( 'Velox binary not found. Please install Velox or ensure it is in your PATH.', dependencyName: self::VELOX_BINARY_NAME, @@ -124,20 +135,21 @@ public function withConfig(VeloxConfig $config, Path $buildDirectory): self } /** - * Checks if the Velox version satisfies the configured constraint. + * Checks if the binary version satisfies the constraint. * * @param Binary $binary The Velox binary to check + * @param string|null $constraint The version constraint to check against * * @return bool True if the version is satisfied, false otherwise */ - private function checkVeloxVersion(Binary $binary): bool + private function checkBinaryVersion(Binary $binary, ?string $constraint): bool { - if ($this->config->veloxVersion === null) { + if ($constraint === null) { return true; } $version = $binary->getVersion(); - $constrain = Constraint::fromConstraintString($this->config->veloxVersion); + $constrain = Constraint::fromConstraintString($constraint); return $version !== null && $constrain->isSatisfiedBy($version); } From fabceecd742dfe8c68aa828beb9d4428277dd4dc Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 23:27:41 +0400 Subject: [PATCH 11/38] refactor(Binary): update execute methods to return output as arrays and add exception handling --- src/Module/Binary/Binary.php | 9 ++++++--- src/Module/Binary/Internal/AbstractBinary.php | 4 ++-- src/Module/Binary/Internal/BinaryExecutor.php | 7 ++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Module/Binary/Binary.php b/src/Module/Binary/Binary.php index e8515fb..a340c06 100644 --- a/src/Module/Binary/Binary.php +++ b/src/Module/Binary/Binary.php @@ -4,6 +4,7 @@ namespace Internal\DLoad\Module\Binary; +use Internal\DLoad\Module\Binary\Exception\BinaryExecutionException; use Internal\DLoad\Module\Common\FileSystem\Path; /** @@ -56,8 +57,10 @@ public function getMTime(): ?\DateTimeImmutable; /** * Executes the binary with the given string input. * - * @param non-empty-string $args Arguments to pass to the binary - * @return string Output from the binary execution + * @param non-empty-string ...$args Arguments to pass to the binary + * @return list Output from the binary execution + * + * @throws BinaryExecutionException If the binary execution returns a non-zero exit code */ - public function execute(string ...$args): string; + public function execute(string ...$args): array; } diff --git a/src/Module/Binary/Internal/AbstractBinary.php b/src/Module/Binary/Internal/AbstractBinary.php index bff047b..70c6063 100644 --- a/src/Module/Binary/Internal/AbstractBinary.php +++ b/src/Module/Binary/Internal/AbstractBinary.php @@ -29,7 +29,7 @@ public function getName(): string return $this->name; } - public function execute(string ...$args): string + public function execute(string ...$args): array { $args = \array_map(static fn(string $arg): string => \escapeshellarg($arg), $args); @@ -81,7 +81,7 @@ public function getVersion(): ?BinaryVersion try { $output = $this->executor->execute($this->getPath(), $this->config->versionCommand); - return $this->versionOutput = BinaryVersion::fromBinaryOutput($output); + return $this->versionOutput = BinaryVersion::fromBinaryOutput(\implode("\n", $output)); } catch (\Throwable) { return $this->versionOutput = BinaryVersion::empty(); } diff --git a/src/Module/Binary/Internal/BinaryExecutor.php b/src/Module/Binary/Internal/BinaryExecutor.php index 31ff34c..625b2cb 100644 --- a/src/Module/Binary/Internal/BinaryExecutor.php +++ b/src/Module/Binary/Internal/BinaryExecutor.php @@ -24,15 +24,16 @@ public function __construct( * * @param Path $binaryPath Full path to binary executable * @param string $command Command argument(s) to execute - * @return string Command output + * @return list Command output * @throws BinaryExecutionException If execution fails */ - public function execute(Path $binaryPath, string $command): string + public function execute(Path $binaryPath, string $command): array { // Escape command for shell execution $escapedPath = \escapeshellarg((string) $binaryPath); // Execute the command and capture output + /** @var list $output */ $output = []; $returnCode = 0; @@ -56,6 +57,6 @@ public function execute(Path $binaryPath, string $command): string } // Return combined output - return \implode("\n", $output); + return $output; } } From a19abc418970915b89a19d05d3a0cc04aa7dc585 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Jul 2025 23:28:03 +0400 Subject: [PATCH 12/38] refactor(DependencyChecker): enhance binary version checks with improved logging and exception handling --- .../Velox/Internal/DependencyChecker.php | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php index fcb02f6..935d41a 100644 --- a/src/Module/Velox/Internal/DependencyChecker.php +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -7,6 +7,7 @@ use Internal\DLoad\Module\Binary\Binary; use Internal\DLoad\Module\Binary\BinaryProvider; use Internal\DLoad\Module\Common\FileSystem\Path; +use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxConfig; use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; use Internal\DLoad\Module\Velox\Exception\Dependency as DependencyException; @@ -33,6 +34,7 @@ final class DependencyChecker public function __construct( private readonly BinaryProvider $binaryProvider, private readonly Logger $logger, + private OperatingSystem $operatingSystem, ) {} /** @@ -89,22 +91,37 @@ public function prepareVelox(): Binary $binaryConfig->versionCommand = '--version'; # Check Velox globally - $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Velox'); - if ($binary !== null && $this->checkBinaryVersion($binary, $this->config->veloxVersion)) { - $this->logger->debug('Found global Velox binary: %s', (string) $binary->getPath()); - return $binary; + try { + $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Velox'); + if ($binary !== null) { + $this->logger->debug('Found global Velox binary: %s', (string) $binary->getPath()->absolute()); + if ($this->checkBinaryVersion($binary, $this->config->veloxVersion)) { + return $binary; + } + + $this->logger->debug( + 'Velox binary version `%s` does not satisfy the required constraint `%s`', + (string) $binary->getVersion(), + (string) $this->config->veloxVersion, + ); + } + } catch (\Throwable) { + // Do nothing } # Check Velox locally - $binary = $this->binaryProvider->getLocalBinary( - $this->veloxPath, - $binaryConfig, - 'Velox', - ); - if ($binary !== null && $this->checkBinaryVersion($binary, $this->config->veloxVersion)) { - $this->logger->debug('Found local Velox binary: %s', (string) $binary->getPath()); - - return $binary; + $binary = $this->binaryProvider->getLocalBinary($this->veloxPath, $binaryConfig, 'Velox'); + if ($binary !== null) { + $this->logger->debug('Found local Velox binary: %s', (string) $binary->getPath()->absolute()); + if ($this->checkBinaryVersion($binary, $this->config->veloxVersion)) { + return $binary; + } + + $this->logger->debug( + 'Velox binary version `%s` does not satisfy the required constraint `%s`', + (string) $binary->getVersion(), + (string) $this->config->veloxVersion, + ); } # todo: download Velox if not installed (execute Download actions) From b253fce1d4f199c69574867e6e07a4179a3a2bdb Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 10:14:21 +0400 Subject: [PATCH 13/38] refactor(LocalBinary): store absolute path for binary in constructor --- src/Module/Binary/Internal/LocalBinary.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Module/Binary/Internal/LocalBinary.php b/src/Module/Binary/Internal/LocalBinary.php index 0ff6698..633a1f4 100644 --- a/src/Module/Binary/Internal/LocalBinary.php +++ b/src/Module/Binary/Internal/LocalBinary.php @@ -14,6 +14,8 @@ */ final class LocalBinary extends AbstractBinary { + private readonly Path $path; + /** * @param non-empty-string $name Binary name * @param Path $path Path to binary @@ -24,8 +26,9 @@ public function __construct( string $name, BinaryConfig $config, BinaryExecutor $executor, - private readonly Path $path, + Path $path, ) { + $this->path = $path->absolute(); parent::__construct($name, $config, $executor); } From a5bd4cf9cab994779e7bf06f581efdbcbd7f18ec Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 11:48:03 +0400 Subject: [PATCH 14/38] refactor(FS, Path): update parameter types for mkdir and add file/directory removal methods --- src/Module/Binary/Binary.php | 2 +- src/Module/Common/FileSystem/FS.php | 83 ++++++++++++++++++++++++++- src/Module/Common/FileSystem/Path.php | 11 +++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/Module/Binary/Binary.php b/src/Module/Binary/Binary.php index a340c06..ce4f54c 100644 --- a/src/Module/Binary/Binary.php +++ b/src/Module/Binary/Binary.php @@ -57,7 +57,7 @@ public function getMTime(): ?\DateTimeImmutable; /** * Executes the binary with the given string input. * - * @param non-empty-string ...$args Arguments to pass to the binary + * @param string ...$args Arguments to pass to the binary * @return list Output from the binary execution * * @throws BinaryExecutionException If the binary execution returns a non-zero exit code diff --git a/src/Module/Common/FileSystem/FS.php b/src/Module/Common/FileSystem/FS.php index 0e6f124..c7febab 100644 --- a/src/Module/Common/FileSystem/FS.php +++ b/src/Module/Common/FileSystem/FS.php @@ -14,14 +14,15 @@ final class FS /** * Creates a directory. * - * @param non-empty-string $path Path to the directory to create + * @param non-empty-string|Path $path Path to the directory to create * @param int $mode Directory permissions (default: 0777) * @param bool $recursive Whether to create parent directories if they do not exist (default: true) * * @throws \RuntimeException If the directory could not be created */ - public static function mkdir(string $path, int $mode = 0777, bool $recursive = true): void + public static function mkdir(string|Path $path, int $mode = 0777, bool $recursive = true): void { + $path = (string) $path; \is_dir($path) or \mkdir($path, $mode, $recursive) or \is_dir($path) or throw new \RuntimeException( \sprintf('Directory "%s" was not created.', $path), ); @@ -43,4 +44,82 @@ public static function tmpDir(?string $path = null, ?string $sub = null): Path return $result; } + + /** + * Removes a file or directory. + * + * @param Path $path Path to the file or directory to remove + * + * @throws \RuntimeException If the file or directory could not be removed + */ + public static function remove(Path $path): bool + { + return !$path->exists() or match (true) { + $path->isFile() => self::removeFile($path), + $path->isDir() => self::removeDir($path), + default => throw new \RuntimeException("Path `{$path->absolute()}` is neither a file nor a directory."), + }; + } + + /** + * Removes a file. + * + * @param Path $path Path to the file to remove + * + * @throws \RuntimeException If the file could not be removed + */ + public static function removeFile(Path $path): bool + { + return \unlink($path->__toString()); + } + + /** + * Removes a directory and all its contents. + * + * @param Path $path Path to the directory to remove + * @param bool $recursive Whether to remove the directory recursively (default: true) + * + * @throws \RuntimeException If the directory could not be removed + */ + public static function removeDir(Path $path, bool $recursive = true): bool + { + if (!$path->exists()) { + return true; + } + + if ($recursive) { + /** @var \DirectoryIterator $item */ + foreach (new \DirectoryIterator($path->__toString()) as $item) { + $item->isDot() or self::remove(Path::create($item->getPathname())); + } + } + + return \rmdir($path->__toString()); + } + + /** + * Moves a file from one path to another. + * + * @param Path $from Source file path + * @param Path $to Destination file path + * @param bool $overwrite Whether to overwrite the destination file if it exists (default: false) + * + * @throws \RuntimeException If the move operation fails + */ + public static function moveFile(Path $from, Path $to, bool $overwrite = false): bool + { + if ($from->absolute() === $to->absolute()) { + return true; // No need to move if paths are the same + } + + if ($to->exists()) { + !$overwrite and throw new \RuntimeException( + "Failed to move file from `{$from}` to `{$to}`: target file already exists.", + ); + + self::remove($to); + } + + return \rename($from->__toString(), $to->__toString()); + } } diff --git a/src/Module/Common/FileSystem/Path.php b/src/Module/Common/FileSystem/Path.php index 89113e0..d85719e 100644 --- a/src/Module/Common/FileSystem/Path.php +++ b/src/Module/Common/FileSystem/Path.php @@ -194,15 +194,20 @@ public function absolute(): self return self::create($cwd . self::DS . $this->path); } + /** + * Return a normalized relative version of this path. + * + * @return non-empty-string + */ public function __toString(): string { return $this->path; } /** - * Check if a path is absolute + * Check if a path is absolute. * - * @param non-empty-string $path A normalized path + * @param non-empty-string $path A normalized path. */ private static function _isAbsolute(string $path): bool { @@ -210,7 +215,7 @@ private static function _isAbsolute(string $path): bool } /** - * Normalize a path by converting directory separators and resolving special path segments + * Normalize a path by converting directory separators and resolving special path segments. * * @return non-empty-string */ From 1742f58ed7aaa42c304f4b2b6b4b107c2d96a557 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 11:50:43 +0400 Subject: [PATCH 15/38] feat(VeloxBuilder): implement basic builder with local config support and binary installation --- src/Module/Velox/Builder.php | 18 +- src/Module/Velox/Internal/VeloxBuilder.php | 195 +++++++++++++++++++++ 2 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 src/Module/Velox/Internal/VeloxBuilder.php diff --git a/src/Module/Velox/Builder.php b/src/Module/Velox/Builder.php index 2b46318..bce0ede 100644 --- a/src/Module/Velox/Builder.php +++ b/src/Module/Velox/Builder.php @@ -4,7 +4,6 @@ namespace Internal\DLoad\Module\Velox; -use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; use Internal\DLoad\Module\Task\Progress; @@ -28,22 +27,7 @@ interface Builder * 5. Cleans up temporary files * * @param VeloxAction $config Build configuration - * @param Path $destination Target directory for the built binary * @param \Closure(Progress): mixed $onProgress Progress callback */ - public function build(VeloxAction $config, Path $destination, \Closure $onProgress): Task; - - /** - * Validates build configuration without executing the build. - * - * Performs preliminary checks: - * - Configuration syntax and completeness - * - Dependency availability - * - Target directory permissions - * - * @param VeloxAction $config Configuration to validate - * @throws Exception\Config When configuration is invalid - * @throws Exception\Dependency When dependencies are unavailable - */ - public function validate(VeloxAction $config): void; + public function build(VeloxAction $config, \Closure $onProgress): Task; } diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php new file mode 100644 index 0000000..a188e02 --- /dev/null +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -0,0 +1,195 @@ +validate($config); + + # Prepare the destination binary path + $destination = Path::create($config->binaryPath ?? '.')->absolute(); + $destination->extension() !== $this->operatingSystem->getBinaryExtension() and $destination = $destination + ->parent() + ->join($destination->stem() . $this->operatingSystem->getBinaryExtension()); + + try { + # Prepare environment + # Create build directory + $buildDir = FS::tmpDir($this->appConfig->tmpDir, 'velox-build'); + + # Check required Dependencies + $dependencyChecker = $this->dependencyChecker->withConfig($config, $buildDir); + # 1. Golang globally + $goBinary = $dependencyChecker->prepareGolang(); + + # 2. Velox locally or globally (downloads if not found) + $vxBinary = $dependencyChecker->prepareVelox(); + + # Prepare configuration file + $configPath = $this->prepareConfig($config, $buildDir); + + # Build + # Execute build command + $builtBinary = $this->executeBuild($configPath, $buildDir, $vxBinary); + # Move built binary to destination + $this->installBinary($builtBinary, $destination); + + $version = Version::fromVersionString($config->binaryVersion); + return resolve(new Result( + binaryPath: $destination, + version: $version, + metadata: [ + 'config_file' => $config->configFile, + 'velox_version' => $vxBinary->getVersion(), + 'golang_version' => $goBinary->getVersion(), + 'binary_version' => $version, + ], + )); + } catch (\Throwable $e) { + $this->logger->error('Build failed: %s', $e->getMessage()); + throw $e; + } finally { + # Remove the build directory + isset($buildDir) and FS::remove($buildDir); + } + }; + + return new Task($config, $onProgress, $handler, 'velox-build'); + } + + private function prepareConfig(VeloxAction $config, Path $buildDir): Path + { + $sourceConfig = Path::create($config->configFile ?? 'velox.toml'); + $targetConfig = $buildDir->join('velox.toml'); + + \copy($sourceConfig->__toString(), $targetConfig->__toString()) or throw new ConfigException( + "Failed to copy config file from `{$sourceConfig}` to `{$targetConfig}`", + configPath: $config->configFile, + ); + + $this->logger->debug('Copied config file to: %s', (string) $targetConfig); + + return $targetConfig; + } + + /** + * Executes the Velox build command with the provided configuration. + * + * @param Path $configPath Path to the Velox configuration file + * @param Path $buildDir Directory where the built binary will be placed + * @param Binary $vxBinary The Velox binary to use for building + * + * @return Path The path to the built binary + * + * @throws BuildException If the build fails or the binary is not found + */ + private function executeBuild(Path $configPath, Path $buildDir, Binary $vxBinary): Path + { + $this->logger->info('Building...'); + $output = $vxBinary->execute( + 'build', + # Specify the build directory + '-o', + $buildDir->absolute()->__toString(), + # Specify the configuration file + '-c', + $configPath->absolute()->__toString(), + ); + + $this->logger->info('Build completed successfully.'); + + // Look for the built binary + return $this->findBuiltBinary($buildDir) ?? throw new BuildException( + 'Built binary not found in the build directory.', + buildOutput: \implode("\n", $output), + ); + } + + /** + * Searches for the built binary in the specified build directory. + * + * @param Path $buildDir The directory where the binary is expected to be found + * + * @return Path|null The path to the built binary, or null if not found + */ + private function findBuiltBinary(Path $buildDir): ?Path + { + // Common locations where velox places built binaries + $searchPaths = [ + $buildDir->join('rr'), + $buildDir->join('rr' . $this->operatingSystem->getBinaryExtension()), + $buildDir->join('roadrunner'), + $buildDir->join('roadrunner' . $this->operatingSystem->getBinaryExtension()), + ]; + + foreach ($searchPaths as $path) { + if ($path->exists() && $path->isFile()) { + $this->logger->debug('Found built binary: %s', (string) $path); + return $path; + } + } + + return null; + } + + /** + * Installs the built binary to the specified destination. + * + * @param Path $builtBinary The path to the built binary + * @param Path $destination The destination path where the binary should be installed + * + * @throws \RuntimeException If the destination cannot be created or the binary cannot be moved + */ + private function installBinary(Path $builtBinary, Path $destination): void + { + # Check if build binary already exists + $destination->exists() + ? FS::remove($destination) + : FS::mkdir($destination->parent()); + + FS::moveFile($builtBinary, $destination); + + // Set executable permissions + \chmod($destination->__toString(), 0755); + + $this->logger->info('Installed binary to: %s', $destination->__toString()); + } +} From 84ecbdf7e1a740f38e82bacae4642c05d3d0c6d3 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 13:28:27 +0400 Subject: [PATCH 16/38] feat(Build): add custom build command for Velox with enhanced logging and error handling --- src/Bootstrap.php | 3 + src/Command/Build.php | 142 ++++++++++++++++++ src/Module/Binary/Internal/BinaryExecutor.php | 1 - src/Module/Binary/Internal/GlobalBinary.php | 2 +- .../Velox/Internal/DependencyChecker.php | 3 - src/Module/Velox/Internal/VeloxBuilder.php | 29 +++- 6 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 src/Command/Build.php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 07e911a..053dad0 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -15,6 +15,8 @@ use Internal\DLoad\Module\HttpClient\Internal\NyholmFactoryImpl; use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubRepositoryFactory; use Internal\DLoad\Module\Repository\RepositoryProvider; +use Internal\DLoad\Module\Velox\Builder; +use Internal\DLoad\Module\Velox\Internal\VeloxBuilder; use Internal\DLoad\Service\Container; /** @@ -106,6 +108,7 @@ public function withConfig( ); $this->container->bind(BinaryProvider::class, BinaryProviderImpl::class); $this->container->bind(Factory::class, NyholmFactoryImpl::class); + $this->container->bind(Builder::class, VeloxBuilder::class); return $this; } diff --git a/src/Command/Build.php b/src/Command/Build.php new file mode 100644 index 0000000..d488371 --- /dev/null +++ b/src/Command/Build.php @@ -0,0 +1,142 @@ +container->get(StyleInterface::class); + + /** @var Actions $actionsConfig */ + $actionsConfig = $this->container->get(Actions::class); + + if ($actionsConfig->veloxBuilds === []) { + $style->warning('No build actions found in configuration file.'); + $style->text('Add actions to your dload.xml to build custom binaries.'); + return Command::SUCCESS; + } + + /** @var Builder $builder */ + $builder = $this->container->get(Builder::class); + + /** @var list $actions */ + $actions = []; + foreach ($actionsConfig->veloxBuilds as $veloxAction) { + $actions[] = $this->prepareBuildAction($builder, $veloxAction, static fn(Progress $progress) => null); + } + + await(all($actions)); + + \count($actions) > 1 and $this->logger->info('All build actions completed.'); + return Command::SUCCESS; + } + + /** + * Gets the destination path as a Path object. + */ + private function getDestinationPath(InputInterface $input): Path + { + /** @var string $pathOption */ + $pathOption = $input->getOption('path'); + return Path::create($pathOption); + } + + /** + * Executes a single Velox build action. + * + * @param callable(Progress): void $onProgress Callback to report progress + */ + private function prepareBuildAction( + Builder $builder, + VeloxAction $veloxAction, + callable $onProgress, + ): PromiseInterface { + $task = $builder->build($veloxAction, $onProgress); + + $this->logger->info('Starting build: %s', $task->name); + + // Execute the build + return $task->execute()->then(onRejected: fn(\Throwable $e) => $this->processException($e, $task)); + } + + /** + * Processes exceptions that occur during the build process. + * + * This method can be overridden to handle specific exceptions + * or perform additional logging. + * + * @param \Throwable $e The exception that occurred + * @param Task $task The task that was being executed when the exception occurred + */ + private function processException(\Throwable $e, Task $task): void + { + $this->logger->error('Build task failed: %s', $task->name); + + if ($e instanceof ConfigException) { + $this->logger->error('Configuration error: %s', $e->getMessage()); + return; + } + + if ($e instanceof DependencyException) { + $this->logger->error('Dependency error: %s', $e->getMessage()); + return; + } + + if ($e instanceof BuildException) { + $this->logger->error('Build error: %s', $e->getMessage()); + if ($e->buildOutput !== null) { + $this->logger->info('Build Output:'); + $this->logger->print($e->buildOutput); + } + + return; + } + + $this->logger->exception($e); + } +} diff --git a/src/Module/Binary/Internal/BinaryExecutor.php b/src/Module/Binary/Internal/BinaryExecutor.php index 625b2cb..ddb2084 100644 --- a/src/Module/Binary/Internal/BinaryExecutor.php +++ b/src/Module/Binary/Internal/BinaryExecutor.php @@ -32,7 +32,6 @@ public function execute(Path $binaryPath, string $command): array // Escape command for shell execution $escapedPath = \escapeshellarg((string) $binaryPath); - // Execute the command and capture output /** @var list $output */ $output = []; $returnCode = 0; diff --git a/src/Module/Binary/Internal/GlobalBinary.php b/src/Module/Binary/Internal/GlobalBinary.php index 6660c6a..6a97d57 100644 --- a/src/Module/Binary/Internal/GlobalBinary.php +++ b/src/Module/Binary/Internal/GlobalBinary.php @@ -72,7 +72,7 @@ private function findBinaryInPath(string $binaryName): ?string $output = []; $returnCode = 0; - \exec("$command $escapedBinaryName", $output, $returnCode); + \exec("$command $escapedBinaryName 2>&1", $output, $returnCode); if ($returnCode !== 0 || $output === []) { return null; diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php index 935d41a..0ea2e9a 100644 --- a/src/Module/Velox/Internal/DependencyChecker.php +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -7,7 +7,6 @@ use Internal\DLoad\Module\Binary\Binary; use Internal\DLoad\Module\Binary\BinaryProvider; use Internal\DLoad\Module\Common\FileSystem\Path; -use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxConfig; use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; use Internal\DLoad\Module\Velox\Exception\Dependency as DependencyException; @@ -29,12 +28,10 @@ final class DependencyChecker private Path $veloxPath; private VeloxConfig $config; - private Path $buildDirectory; public function __construct( private readonly BinaryProvider $binaryProvider, private readonly Logger $logger, - private OperatingSystem $operatingSystem, ) {} /** diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php index a188e02..be6bded 100644 --- a/src/Module/Velox/Internal/VeloxBuilder.php +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -42,10 +42,8 @@ public function __construct( public function build(VeloxAction $config, \Closure $onProgress): Task { $handler = function () use ($config, $onProgress): PromiseInterface { - $this->validate($config); - # Prepare the destination binary path - $destination = Path::create($config->binaryPath ?? '.')->absolute(); + $destination = Path::create($config->binaryPath ?? 'rr')->absolute(); $destination->extension() !== $this->operatingSystem->getBinaryExtension() and $destination = $destination ->parent() ->join($destination->stem() . $this->operatingSystem->getBinaryExtension()); @@ -92,7 +90,7 @@ public function build(VeloxAction $config, \Closure $onProgress): Task } }; - return new Task($config, $onProgress, $handler, 'velox-build'); + return new Task($config, $onProgress, $handler, $this->getBuildName($config)); } private function prepareConfig(VeloxAction $config, Path $buildDir): Path @@ -192,4 +190,27 @@ private function installBinary(Path $builtBinary, Path $destination): void $this->logger->info('Installed binary to: %s', $destination->__toString()); } + + /** + * Gets a descriptive name for the build action. + */ + private function getBuildName(VeloxAction $veloxAction): string + { + if ($veloxAction->configFile !== null) { + return "Velox build (config: {$veloxAction->configFile})"; + } + + if ($veloxAction->plugins !== []) { + $pluginNames = \array_map(static fn($plugin) => $plugin->name, $veloxAction->plugins); + $pluginCount = \count($pluginNames); + + if ($pluginCount <= 3) { + return 'Velox build (plugins: ' . \implode(', ', $pluginNames) . ')'; + } + + return "Velox build ({$pluginCount} plugins)"; + } + + return 'Velox build'; + } } From 39371e75491a080d558bdb4bcdee2dc81d11a87d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 15:59:55 +0400 Subject: [PATCH 17/38] feat(Manager): enhance task management with promise support and deferred resolution --- src/Module/Task/Manager.php | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Module/Task/Manager.php b/src/Module/Task/Manager.php index 5768afa..a1a69e3 100644 --- a/src/Module/Task/Manager.php +++ b/src/Module/Task/Manager.php @@ -5,6 +5,8 @@ namespace Internal\DLoad\Module\Task; use Internal\DLoad\Service\Logger; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; /** * Task Manager Service @@ -29,7 +31,7 @@ */ final class Manager { - /** @var array<\Fiber> Active fiber tasks */ + /** @var array{\Fiber, Deferred} Active fiber tasks */ private array $tasks = []; /** @@ -44,11 +46,17 @@ public function __construct( /** * Adds a new task to the execution queue * - * @param \Closure $callback Task implementation + * @template TResult + * + * @param \Closure(): TResult $callback Task implementation + * + * @return PromiseInterface */ - public function addTask(\Closure $callback): void + public function addTask(\Closure $callback): PromiseInterface { - $this->tasks[] = new \Fiber($callback); + $deferred = new Deferred(); + $this->tasks[] = [new \Fiber($callback), $deferred]; + return $deferred->promise(); } /** @@ -66,10 +74,15 @@ public function getProcessor(): \Generator return; } - foreach ($this->tasks as $key => $task) { + /** + * @var \Fiber $task + * @var Deferred $deferred + */ + foreach ($this->tasks as $key => [$task, $deferred]) { try { if ($task->isTerminated()) { unset($this->tasks[$key]); + $deferred->resolve($task->getReturn()); continue; } @@ -83,6 +96,7 @@ public function getProcessor(): \Generator $this->logger->error($e->getMessage()); $this->logger->exception($e); unset($this->tasks[$key]); + $deferred->reject($e); yield $e; } } From dd8593817cf7a710afc097c515812e7ac9101cf6 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 16:25:49 +0400 Subject: [PATCH 18/38] feat(DLoad): enhance download task with promise support and introduce DloadResult class --- src/DLoad.php | 219 +++++++++++++++++------------- src/Module/Common/DloadResult.php | 33 +++++ 2 files changed, 157 insertions(+), 95 deletions(-) create mode 100644 src/Module/Common/DloadResult.php diff --git a/src/DLoad.php b/src/DLoad.php index 2dd7947..6c50180 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -6,11 +6,14 @@ use Internal\DLoad\Module\Archive\ArchiveFactory; use Internal\DLoad\Module\Binary\BinaryProvider; +use Internal\DLoad\Module\Common\DloadResult; +use Internal\DLoad\Module\Common\FileSystem\FS; use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Common\Input\Destination; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Config\Schema\Action\Download as DownloadConfig; use Internal\DLoad\Module\Config\Schema\Action\Type; +use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; use Internal\DLoad\Module\Config\Schema\Embed\File; use Internal\DLoad\Module\Config\Schema\Embed\Software; use Internal\DLoad\Module\Downloader\Downloader; @@ -24,6 +27,7 @@ use React\Promise\PromiseInterface; use Symfony\Component\Console\Output\OutputInterface; +use function React\Async\await; use function React\Promise\resolve; /** @@ -65,9 +69,12 @@ public function __construct( * * @param DownloadConfig $action Download configuration action * @param bool $force Whether to force download even if binary exists + * + * @return PromiseInterface Resolves after the download task is finished. + * * @throws \RuntimeException When software package is not found */ - public function addTask(DownloadConfig $action, bool $force = false): void + public function addTask(DownloadConfig $action, bool $force = false): PromiseInterface { // Find Software $software = $this->softwareCollection->findSoftware($action->software) ?? throw new \RuntimeException( @@ -97,7 +104,7 @@ public function addTask(DownloadConfig $action, bool $force = false): void $this->logger->info('Use flag `--force` to force download.'); // Skip task creation entirely - return; + return resolve(DloadResult::fromBinary($binary)); } // Create VersionConstraint DTO for enhanced constraint checking @@ -115,7 +122,7 @@ public function addTask(DownloadConfig $action, bool $force = false): void $this->logger->info('Use flag `--force` to force download.'); // Skip task creation entirely - return; + return resolve(DloadResult::fromBinary($binary)); } // Download a newer version only if the version is specified @@ -126,12 +133,16 @@ public function addTask(DownloadConfig $action, bool $force = false): void add_task: - $this->taskManager->addTask(function () use ($software, $action): void { + return $this->taskManager->addTask(function () use ($software, $action): DloadResult { // Create a Download task $task = $this->prepareDownloadTask($software, $action); // Extract files - ($task->handler)()->then($this->prepareExtractTask($software, $action)); + $extraction = ($task->handler)()->then( + fn(DownloadResult $result): DloadResult => $this->prepareExtractTask($result, $software, $action), + ); + + return await($extraction); }); } @@ -177,83 +188,105 @@ private function prepareDownloadTask(Software $software, DownloadConfig $action) * @param DownloadConfig $action Download action configuration * @return \Closure(DownloadResult): void Function that extracts files from the downloaded archive */ - private function prepareExtractTask(Software $software, DownloadConfig $action): \Closure + private function prepareExtractTask(DownloadResult $downloadResult, Software $software, DownloadConfig $action): DloadResult { - return function (DownloadResult $downloadResult) use ($software, $action): void { - $fileInfo = $downloadResult->file; - $tempFilePath = $fileInfo->getRealPath() ?: $fileInfo->getPathname(); - - try { - // Create a copy of the files list with binary included if necessary - $files = $this->filesToExtract($software, $action); - - // Create destination directory if it doesn't exist - $path = $this->getDestinationPath($action); - if (!\is_dir((string) $path)) { - $this->logger->info('Creating directory %s', (string) $path); - @\mkdir((string) $path, 0755, true); - } + $fileInfo = $downloadResult->file; + $tempFilePath = Path::create($fileInfo->getRealPath() ?: $fileInfo->getPathname()); + $resultFiles = []; + $resultBinary = null; + + try { + # Create destination directory if it doesn't exist + $destination = $this->getDestinationPath($action); + FS::mkdir($destination); + + # In PHAR actions, we do not extract files, just copy the downloaded file + if ($action->type === Type::Phar) { + $this->logger->debug( + 'Copying downloaded file `%s` to destination as a PHAR archive.', + $fileInfo->getFilename(), + ); + $toFile = $destination->join($fileInfo->getFilename()); + FS::moveFile($tempFilePath, $toFile); + \chmod((string) $toFile, 0o755); + + # todo: add PHAR binary to result + return new DloadResult([$toFile]); + } - // If no extraction rules are defined, do not extract anything - // and just copy the file to the destination - if ($files === []) { - $this->logger->debug( - 'No files to extract for `%s`, copying the downloaded file to the destination.', - $fileInfo->getFilename(), - ); - $toFile = (string) $path->join($fileInfo->getFilename()); - \copy($tempFilePath, $toFile); - - $action->type === Type::Phar and \chmod($toFile, 0o755); - return; + # If no extraction rules are defined, do not extract anything + # and just copy the file to the destination + if ($software->files === [] && $software->binary === null) { + $this->logger->debug( + 'No files to extract for `%s`, copying the downloaded file to the destination.', + $fileInfo->getFilename(), + ); + $toFile = $destination->join($fileInfo->getFilename()); + FS::moveFile($tempFilePath, $toFile); + + return new DloadResult([$toFile]); + } + + $archive = $this->archiveFactory->create($fileInfo); + $extractor = $archive->extract(); + $this->logger->info('Extracting %s', $fileInfo->getFilename()); + $binaryPattern = $this->generateBinaryExtractionConfig($software->binary); + + while ($extractor->valid()) { + $file = $extractor->current(); + \assert($file instanceof \SplFileInfo); + + # Check if it's binary and should be extracted + $isBinary = false; + if ($binaryPattern !== null) { + [$to, $rule] = $this->shouldBeExtracted($file, [$binaryPattern], $destination); + $isBinary = $to !== null; } - $archive = $this->archiveFactory->create($fileInfo); - $extractor = $archive->extract(); - $this->logger->info('Extracting %s', $fileInfo->getFilename()); - - while ($extractor->valid()) { - $file = $extractor->current(); - \assert($file instanceof \SplFileInfo); - - [$to, $rule] = $this->shouldBeExtracted($file, $files, $action); - $this->logger->debug( - $to === null ? 'Skipping %s%s' : 'Extracting %s to %s', - $file->getFilename(), - (string) $to?->getPathname(), - ); - - if ($to === null) { - $extractor->next(); - continue; - } - - $isOverwriting = $to->isFile(); - $extractor->send($to); - - // Success - $path = $to->getRealPath() ?: $to->getPathname(); - $this->output->writeln( - \sprintf( - '%s (%s) has been %sinstalled into %s', - $to->getFilename(), - $downloadResult->version, - $isOverwriting ? 're' : '', - $path, - ), - ); - - \assert($rule !== null); - $rule->chmod === null or @\chmod($path, $rule->chmod); + $isBinary or [$to, $rule] = $this->shouldBeExtracted($file, $software->files, $destination); + if ($to === null) { + $this->logger->debug('Skipping file `%s`.', $file->getFilename()); + $extractor->next(); + continue; } - } finally { - // Cleanup: Delete the temporary downloaded file - if (!$this->useMock && \file_exists($tempFilePath)) { - $this->logger->debug('Cleaning up temporary file: %s', $tempFilePath); - @\unlink($tempFilePath); + + $this->logger->debug('Extracting %s to %s...', $file->getFilename(), $to->getPathname()); + + $isOverwriting = $to->isFile(); + $extractor->send($to); + + // Success + $path = $to->getRealPath() ?: $to->getPathname(); + $this->output->writeln( + \sprintf( + '%s (%s) has been %sinstalled into %s', + $to->getFilename(), + $downloadResult->version, + $isOverwriting ? 're' : '', + $path, + ), + ); + + \assert($rule !== null); + $rule->chmod === null or @\chmod($path, $rule->chmod); + + # Add files and binary to result + $path = Path::create($path); + $resultFiles[] = $path; + if ($isBinary) { + $resultBinary = $this->binaryProvider->getLocalBinary($path->parent(), $software->binary); + $binaryPattern = null; } } - }; + + return new DloadResult($resultFiles, $resultBinary); + } finally { + // Cleanup: Delete the temporary downloaded file + if (!$this->useMock && $tempFilePath->exists()) { + $this->logger->debug('Cleaning up temporary file: %s', $tempFilePath->__toString()); + FS::remove($tempFilePath); + } + } } /** @@ -261,15 +294,13 @@ private function prepareExtractTask(Software $software, DownloadConfig $action): * * @param \SplFileInfo $source Source file from the archive * @param list $mapping File mapping configurations - * @param DownloadConfig $action Download action configuration - * @return array{\SplFileInfo|null, null|File} Array containing: + * @param Path $path Destination path where files should be extracted + * @return array{\SplFileInfo, File}|array{null, null} Array containing: * - Target file path or null if file should not be extracted * - File configuration that matched the source file, or null if no match found */ - private function shouldBeExtracted(\SplFileInfo $source, array $mapping, DownloadConfig $action): array + private function shouldBeExtracted(\SplFileInfo $source, array $mapping, Path $path): array { - $path = $this->getDestinationPath($action); - foreach ($mapping as $conf) { if (\preg_match($conf->pattern, $source->getFilename())) { $newName = match (true) { @@ -296,25 +327,23 @@ private function getDestinationPath(DownloadConfig $action): Path } /** - * @return list + * Generates a binary extraction configuration based on the provided binary configuration. + * + * @param BinaryConfig|null $binary Binary configuration object + * @return File|null File extraction configuration or null if no binary is provided */ - private function filesToExtract(Software $software, DownloadConfig $action): array + private function generateBinaryExtractionConfig(?BinaryConfig $binary): ?File { - // Don't extract files for Phar actions - if ($action->type === Type::Phar) { - return []; + if ($binary === null) { + return null; } - $files = $software->files; - if ($software->binary !== null) { - $binary = new File(); - $binary->pattern = $software->binary->pattern - ?? "/^{$software->binary->name}{$this->os->getBinaryExtension()}$/"; - $binary->rename = $software->binary->name; - $binary->chmod = 0o755; // Default permissions for binaries - $files[] = $binary; - } + $result = new File(); + $result->pattern = $binary->pattern + ?? "/^{$binary->name}{$this->os->getBinaryExtension()}$/"; + $result->rename = $binary->name; + $result->chmod = 0o755; // Default permissions for binaries - return $files; + return $result; } } diff --git a/src/Module/Common/DloadResult.php b/src/Module/Common/DloadResult.php new file mode 100644 index 0000000..1256f5d --- /dev/null +++ b/src/Module/Common/DloadResult.php @@ -0,0 +1,33 @@ + $files All the downloaded files. + * @param Binary|null $binary Optional binary file if available. + */ + public function __construct( + public readonly array $files, + public readonly ?Binary $binary = null, + ) {} + + public static function empty(): self + { + return new self([]); + } + + public static function fromBinary(Binary $binary): self + { + return new self([$binary->getPath()], $binary); + } +} From ab02fe6273470b5e4f69ba761492a21f5fe124dd Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 17:06:13 +0400 Subject: [PATCH 19/38] feat(DependencyChecker): implement Velox binary download logic with version constraint checks --- bin/dload | 1 + .../Velox/Internal/DependencyChecker.php | 203 +++++++++++++++++- 2 files changed, 196 insertions(+), 8 deletions(-) diff --git a/bin/dload b/bin/dload index 419f400..9eca91d 100644 --- a/bin/dload +++ b/bin/dload @@ -50,6 +50,7 @@ use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; Command\ListSoftware::getDefaultName() => static fn() => new Command\ListSoftware(), Command\Show::getDefaultName() => static fn() => new Command\Show(), Command\Init::getDefaultName() => static fn() => new Command\Init(), + Command\Build::getDefaultName() => static fn() => new Command\Build(), ]), ); $application->setDefaultCommand(Command\Get::getDefaultName(), false); diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php index 0ea2e9a..69e37ed 100644 --- a/src/Module/Velox/Internal/DependencyChecker.php +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -4,15 +4,24 @@ namespace Internal\DLoad\Module\Velox\Internal; +use Internal\DLoad\DLoad; use Internal\DLoad\Module\Binary\Binary; use Internal\DLoad\Module\Binary\BinaryProvider; +use Internal\DLoad\Module\Common\DloadResult; use Internal\DLoad\Module\Common\FileSystem\Path; +use Internal\DLoad\Module\Config\Schema\Action\Download; +use Internal\DLoad\Module\Config\Schema\Action\Type as DownloadType; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxConfig; +use Internal\DLoad\Module\Config\Schema\Actions; use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; +use Internal\DLoad\Module\Config\Schema\Embed\Software; +use Internal\DLoad\Module\Downloader\SoftwareCollection; use Internal\DLoad\Module\Velox\Exception\Dependency as DependencyException; use Internal\DLoad\Module\Version\Constraint; use Internal\DLoad\Service\Logger; +use function React\Async\await; + /** * Dependency checker for Velox build requirements. * @@ -28,10 +37,14 @@ final class DependencyChecker private Path $veloxPath; private VeloxConfig $config; + private Path $buildDirectory; public function __construct( private readonly BinaryProvider $binaryProvider, private readonly Logger $logger, + private readonly SoftwareCollection $softwareCollection, + private readonly Actions $actions, + private readonly DLoad $downloader, ) {} /** @@ -83,15 +96,23 @@ public function prepareVelox(): Binary $this->checkServiceState(); # Prepare config - $binaryConfig = new BinaryConfig(); - $binaryConfig->name = self::VELOX_BINARY_NAME; - $binaryConfig->versionCommand = '--version'; + $softwareConfig = $this->softwareCollection->findSoftware('velox'); + $binaryConfig = $softwareConfig?->binary; + if ($binaryConfig === null) { + $binaryConfig = new BinaryConfig(); + $binaryConfig->name = 'vx'; + $binaryConfig->versionCommand = '--version'; + } # Check Velox globally try { $binary = $this->binaryProvider->getGlobalBinary($binaryConfig, 'Velox'); if ($binary !== null) { - $this->logger->debug('Found global Velox binary: %s', (string) $binary->getPath()->absolute()); + $this->logger->debug( + 'Found global Velox binary: %s (%s)', + (string) $binary->getPath()->absolute(), + (string) $binary->getVersion(), + ); if ($this->checkBinaryVersion($binary, $this->config->veloxVersion)) { return $binary; } @@ -107,9 +128,14 @@ public function prepareVelox(): Binary } # Check Velox locally + # 1. Check local binary $binary = $this->binaryProvider->getLocalBinary($this->veloxPath, $binaryConfig, 'Velox'); if ($binary !== null) { - $this->logger->debug('Found local Velox binary: %s', (string) $binary->getPath()->absolute()); + $this->logger->debug( + 'Found local Velox binary: %s (%s)', + (string) $binary->getPath()->absolute(), + (string) $binary->getVersion(), + ); if ($this->checkBinaryVersion($binary, $this->config->veloxVersion)) { return $binary; } @@ -121,13 +147,46 @@ public function prepareVelox(): Binary ); } - # todo: download Velox if not installed (execute Download actions) - # todo: check download actions + # 2. Check download actions + /** @var list $softwareList */ + $downloads = []; + $binary = $this->checkDownloadedVelox($softwareConfig, $downloads); + if ($binary !== null) { + $this->logger->debug( + 'Found downloaded Velox binary: %s (%s)', + (string) $binary->getPath()->absolute(), + (string) $binary->getVersion(), + ); + return $binary; + } + + # 3. If no binaries are found, download Velox + $softwareConfig === null and throw new DependencyException( + 'Velox software configuration not found. Please ensure Velox is defined in your configuration.', + dependencyName: 'Velox', + ); + + # Add a download action if no actions are configured + $fallbackAction = Download::fromSoftwareId('velox'); + $fallbackAction->version = $this->config->veloxVersion; + $fallbackAction->extractPath = $this->buildDirectory->__toString(); + $fallbackAction->type = DownloadType::Binary; + + $this->logger->info('Downloading Velox binary...'); + $binary = $this->downloadVelox($softwareConfig, $downloads, $fallbackAction); + if ($binary !== null) { + $this->logger->debug( + 'Downloaded Velox binary: %s (%s)', + (string) $binary->getPath()->absolute(), + (string) $binary->getVersion(), + ); + return $binary; + } # Throw exception if Velox is not found throw new DependencyException( 'Velox binary not found. Please install Velox or ensure it is in your PATH.', - dependencyName: self::VELOX_BINARY_NAME, + dependencyName: 'Velox', ); } @@ -180,4 +239,132 @@ private function checkServiceState(): void 'Velox configuration is not set. Use `withConfig()` to set it before checking dependencies.', ); } + + /** + * Checks if Velox is downloaded and returns the binary if available. + * + * @param Software|null $softwareConfig The software configuration for Velox + * @param list $downloads The list of Velox download actions + * + * @return Binary|null The Velox binary if found, null otherwise + */ + private function checkDownloadedVelox(?Software $softwareConfig, array &$downloads): ?Binary + { + $binaryConfig = $softwareConfig?->binary; + if ($softwareConfig === null || $binaryConfig === null) { + return null; + } + + # Find the relevant download action for Velox + foreach ($this->actions->downloads as $download) { + $software = $this->softwareCollection->findSoftware($download->software); + if ($software !== $softwareConfig) { + continue; + } + + # Get binary + $binary = $this->binaryProvider + ->getLocalBinary(Path::create($download->extractPath ?? '.'), $binaryConfig, $softwareConfig->name); + + if ($binary === null) { + $downloads[] = $download; + continue; + } + + if ($this->config->veloxVersion === null) { + return $binary; + } + + # Check version constraint + $constraint = Constraint::fromConstraintString($this->config->veloxVersion); + $binaryVersion = $binary->getVersion(); + + if ($binaryVersion === null || !$constraint->isSatisfiedBy($binaryVersion)) { + $this->logger->debug( + 'Found downloaded Velox binary in `%s` but version `%s` does not satisfy constraint `%s`', + $binary->getPath()->absolute()->__toString(), + (string) $binaryVersion, + $constraint->__toString(), + ); + + # If local binary does not satisfy the download version constraint, + # then we can redownload it + if ($binaryVersion !== null && $download->version !== null) { + Constraint::fromConstraintString($download->version) + ->isSatisfiedBy($binaryVersion) or $downloads[] = $download; + } + + continue; + } + + return $binary; + } + + return null; + } + + /** + * Downloads the Velox binary based on the provided download actions. + * If no downloads are configured or the download fails on version constraint, + * it uses a fallback action to download Velox. + * + * @param Software $software The software configuration for Velox + * @param list $downloads The list of download actions for Velox + * @param Download $fallbackAction The fallback download action if no downloads are configured + * + * @return Binary|null The downloaded Velox binary or null + * + * @throws DependencyException If the download fails or the binary is not found + */ + private function downloadVelox(Software $software, array $downloads, Download $fallbackAction): ?Binary + { + $usedFallback = false; + + try_download: + foreach ($downloads as $download) { + try { + $promise = $this->downloader->addTask($download, force: true); + $this->downloader->run(); + + /** @var DloadResult $result */ + $result = await($promise); + + # Check if the binary download was successful + $binary = $result->binary; + if ($binary === null) { + $this->logger->debug('Download for Velox binary failed: no binary found.'); + continue; + } + + $constraint = $this->config->veloxVersion === null + ? null + : Constraint::fromConstraintString($this->config->veloxVersion); + $binaryVersion = $binary->getVersion(); + + # Check if the downloaded binary satisfies the version constraint + if ($constraint === null or $binaryVersion !== null && $constraint->isSatisfiedBy($binaryVersion)) { + return $binary; + } + + $this->logger->debug( + 'Downloaded Velox binary `%s` with version `%s` does not satisfy the constraint `%s`', + $binary->getPath()->__toString(), + (string) $binaryVersion, + (string) $this->config->veloxVersion, + ); + continue; + } catch (\Throwable) { + continue; + } + } + + if ($usedFallback) { + return null; // No valid binary found after trying all downloads + } + + # If no downloads were successful, use the fallback action + $usedFallback = true; + $downloads = [$fallbackAction]; + goto try_download; + } } From 83f44f3af8745acded2e7e10f5bcda50cae6dce2 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 17:07:26 +0400 Subject: [PATCH 20/38] feat(VeloxBuilder): modify error handling to return rejected promise on build failure --- src/Module/Velox/Internal/VeloxBuilder.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php index be6bded..8134d20 100644 --- a/src/Module/Velox/Internal/VeloxBuilder.php +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -19,6 +19,7 @@ use Internal\DLoad\Service\Logger; use React\Promise\PromiseInterface; +use function React\Promise\reject; use function React\Promise\resolve; /** @@ -55,6 +56,7 @@ public function build(VeloxAction $config, \Closure $onProgress): Task # Check required Dependencies $dependencyChecker = $this->dependencyChecker->withConfig($config, $buildDir); + # 1. Golang globally $goBinary = $dependencyChecker->prepareGolang(); @@ -82,8 +84,7 @@ public function build(VeloxAction $config, \Closure $onProgress): Task ], )); } catch (\Throwable $e) { - $this->logger->error('Build failed: %s', $e->getMessage()); - throw $e; + return reject($e); } finally { # Remove the build directory isset($buildDir) and FS::remove($buildDir); From 7f3609df01ddb66dbc51908258d884765be4a354 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 17:18:08 +0400 Subject: [PATCH 21/38] feat(DLoad): add binary-path attribute to specify RoadRunner binary location --- dload.xsd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dload.xsd b/dload.xsd index ee4dd06..fec2cc7 100644 --- a/dload.xsd +++ b/dload.xsd @@ -118,6 +118,11 @@ Path to local velox.toml file + + + Path to the RoadRunner binary to build + + From edae512eae012d683d1e0f48308093488f401332 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Fri, 11 Jul 2025 17:58:51 +0400 Subject: [PATCH 22/38] chore: cleanup; fix psalm issues --- composer.lock | 125 +++++++++--------- psalm-baseline.xml | 90 ++++++------- psalm.xml | 5 + src/Command/Build.php | 16 +-- src/DLoad.php | 13 +- src/Module/Binary/BinaryVersion.php | 2 +- src/Module/Common/FileSystem/Path.php | 2 + .../Common/Internal/ObjectContainer.php | 6 +- src/Module/Task/Manager.php | 2 +- .../Velox/Internal/DependencyChecker.php | 15 ++- src/Module/Velox/Internal/VeloxBuilder.php | 49 +++++-- src/Module/Velox/Result.php | 59 +-------- src/Module/Velox/Task.php | 2 +- 13 files changed, 175 insertions(+), 211 deletions(-) diff --git a/composer.lock b/composer.lock index 347c577..1154bbd 100644 --- a/composer.lock +++ b/composer.lock @@ -650,16 +650,16 @@ }, { "name": "symfony/console", - "version": "v6.4.22", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3" + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", - "reference": "7d29659bc3c9d8e9a34e2c3414ef9e9e003e6cf3", + "url": "https://api.github.com/repos/symfony/console/zipball/9056771b8eca08d026cd3280deeec3cfd99c4d93", + "reference": "9056771b8eca08d026cd3280deeec3cfd99c4d93", "shasum": "" }, "require": { @@ -724,7 +724,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.22" + "source": "https://github.com/symfony/console/tree/v6.4.23" }, "funding": [ { @@ -740,7 +740,7 @@ "type": "tidelift" } ], - "time": "2025-05-07T07:05:04+00:00" + "time": "2025-06-27T19:37:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -811,16 +811,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.19", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "3294a433fc9d12ae58128174896b5b1822c28dad" + "reference": "19f11e742b94dcfd968a54f5381bb9082a88cb57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3294a433fc9d12ae58128174896b5b1822c28dad", - "reference": "3294a433fc9d12ae58128174896b5b1822c28dad", + "url": "https://api.github.com/repos/symfony/http-client/zipball/19f11e742b94dcfd968a54f5381bb9082a88cb57", + "reference": "19f11e742b94dcfd968a54f5381bb9082a88cb57", "shasum": "" }, "require": { @@ -884,7 +884,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.19" + "source": "https://github.com/symfony/http-client/tree/v6.4.23" }, "funding": [ { @@ -900,7 +900,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T09:55:13+00:00" + "time": "2025-06-27T20:02:31+00:00" }, { "name": "symfony/http-client-contracts", @@ -3121,58 +3121,59 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.75.0", + "version": "v3.82.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c" + "reference": "684ed3ab41008a2a4848de8bde17eb168c596247" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/399a128ff2fdaf4281e4e79b755693286cdf325c", - "reference": "399a128ff2fdaf4281e4e79b755693286cdf325c", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/684ed3ab41008a2a4848de8bde17eb168c596247", + "reference": "684ed3ab41008a2a4848de8bde17eb168c596247", "shasum": "" }, "require": { "clue/ndjson-react": "^1.0", "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", + "composer/xdebug-handler": "^3.0.5", "ext-filter": "*", "ext-hash": "*", "ext-json": "*", "ext-tokenizer": "*", "fidry/cpu-core-counter": "^1.2", "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", + "react/child-process": "^0.6.6", "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", + "react/promise": "^2.11 || ^3.0", "react/socket": "^1.0", "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", + "symfony/console": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", + "symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", + "symfony/polyfill-mbstring": "^1.32", + "symfony/polyfill-php80": "^1.32", + "symfony/polyfill-php81": "^1.32", + "symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", + "symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.6", "infection/infection": "^0.29.14", - "justinrainbow/json-schema": "^5.3 || ^6.2", - "keradus/cli-executor": "^2.1", + "justinrainbow/json-schema": "^5.3 || ^6.4", + "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", + "php-coveralls/php-coveralls": "^2.8", "php-cs-fixer/accessible-object": "^1.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", - "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", - "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" + "phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", + "symfony/polyfill-php84": "^1.32", + "symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", + "symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -3213,7 +3214,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.75.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.82.2" }, "funding": [ { @@ -3221,7 +3222,7 @@ "type": "github" } ], - "time": "2025-03-31T18:40:42+00:00" + "time": "2025-07-08T21:13:15+00:00" }, { "name": "kelunik/certificate", @@ -3457,16 +3458,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -3505,7 +3506,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -3513,7 +3514,7 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "netresearch/jsonmapper", @@ -4441,16 +4442,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.47", + "version": "10.5.48", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3" + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3", - "reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6e0a2bc39f6fae7617989d690d76c48e6d2eb541", + "reference": "6e0a2bc39f6fae7617989d690d76c48e6d2eb541", "shasum": "" }, "require": { @@ -4460,7 +4461,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.3", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -4522,7 +4523,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.47" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.48" }, "funding": [ { @@ -4546,7 +4547,7 @@ "type": "tidelift" } ], - "time": "2025-06-20T11:29:11+00:00" + "time": "2025-07-11T04:07:17+00:00" }, { "name": "psr/event-dispatcher", @@ -6801,16 +6802,16 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.21", + "version": "v6.4.23", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5" + "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", - "reference": "22560f80c0c5cd58cc0bcaf73455ffd81eb380d5", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600", + "reference": "d55b1834cdbfcc31bc2cd7e095ba5ed9a88f6600", "shasum": "" }, "require": { @@ -6866,7 +6867,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.21" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.23" }, "funding": [ { @@ -6882,7 +6883,7 @@ "type": "tidelift" } ], - "time": "2025-04-09T07:34:50+00:00" + "time": "2025-06-27T15:05:27+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -6995,16 +6996,16 @@ }, { "name": "vimeo/psalm", - "version": "6.12.0", + "version": "6.12.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "cf420941d061a57050b6c468ef2c778faf40aee2" + "reference": "e71404b0465be25cf7f8a631b298c01c5ddd864f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/cf420941d061a57050b6c468ef2c778faf40aee2", - "reference": "cf420941d061a57050b6c468ef2c778faf40aee2", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/e71404b0465be25cf7f8a631b298c01c5ddd864f", + "reference": "e71404b0465be25cf7f8a631b298c01c5ddd864f", "shasum": "" }, "require": { @@ -7109,7 +7110,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2025-05-28T12:52:06+00:00" + "time": "2025-07-04T09:56:28+00:00" }, { "name": "webmozart/assert", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 05d441f..3b4f7fd 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -79,9 +79,17 @@ - - - + + + + chmod]]> + + + chmod]]> + + + binary]]> + @@ -125,15 +133,28 @@ - + + + + + ]]> + - - - - + + + + + + + + + + + + @@ -149,9 +170,15 @@ + + + + + + factory]]> @@ -162,6 +189,9 @@ cache]]> + + injector->invoke([$binding, 'create'])]]> + @@ -172,26 +202,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -315,11 +325,6 @@ && \is_string($decoded[1])]]> - - - - - @@ -361,15 +366,6 @@ - - - - - getRepository()->getName(), - $this->getVersion(), - ]]]> - assets as $assetDTO) { @@ -393,14 +389,6 @@ - - - - - - isSatisfiedBy($this->version)]]> diff --git a/psalm.xml b/psalm.xml index 212781f..9885caa 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,6 +12,11 @@ + + + + + diff --git a/src/Command/Build.php b/src/Command/Build.php index d488371..bf8ad26 100644 --- a/src/Command/Build.php +++ b/src/Command/Build.php @@ -12,6 +12,7 @@ use Internal\DLoad\Module\Velox\Exception\Build as BuildException; use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; use Internal\DLoad\Module\Velox\Exception\Dependency as DependencyException; +use Internal\DLoad\Module\Velox\Result; use Internal\DLoad\Module\Velox\Task; use React\Promise\PromiseInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -76,25 +77,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - /** - * Gets the destination path as a Path object. - */ - private function getDestinationPath(InputInterface $input): Path - { - /** @var string $pathOption */ - $pathOption = $input->getOption('path'); - return Path::create($pathOption); - } - /** * Executes a single Velox build action. * - * @param callable(Progress): void $onProgress Callback to report progress + * @param \Closure(Progress): void $onProgress Callback to report progress + * @return PromiseInterface Promise that resolves to the build result or null if no binary was built */ private function prepareBuildAction( Builder $builder, VeloxAction $veloxAction, - callable $onProgress, + \Closure $onProgress, ): PromiseInterface { $task = $builder->build($veloxAction, $onProgress); diff --git a/src/DLoad.php b/src/DLoad.php index 6c50180..06e9911 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -186,10 +186,13 @@ private function prepareDownloadTask(Software $software, DownloadConfig $action) * * @param Software $software Software package configuration * @param DownloadConfig $action Download action configuration - * @return \Closure(DownloadResult): void Function that extracts files from the downloaded archive + * @return DloadResult Result of the extraction process containing extracted files and binary */ - private function prepareExtractTask(DownloadResult $downloadResult, Software $software, DownloadConfig $action): DloadResult - { + private function prepareExtractTask( + DownloadResult $downloadResult, + Software $software, + DownloadConfig $action, + ): DloadResult { $fileInfo = $downloadResult->file; $tempFilePath = Path::create($fileInfo->getRealPath() ?: $fileInfo->getPathname()); $resultFiles = []; @@ -243,7 +246,7 @@ private function prepareExtractTask(DownloadResult $downloadResult, Software $so $isBinary = $to !== null; } - $isBinary or [$to, $rule] = $this->shouldBeExtracted($file, $software->files, $destination); + isset($to) or [$to, $rule] = $this->shouldBeExtracted($file, $software->files, $destination); if ($to === null) { $this->logger->debug('Skipping file `%s`.', $file->getFilename()); $extractor->next(); @@ -267,7 +270,7 @@ private function prepareExtractTask(DownloadResult $downloadResult, Software $so ), ); - \assert($rule !== null); + \assert(isset($rule)); $rule->chmod === null or @\chmod($path, $rule->chmod); # Add files and binary to result diff --git a/src/Module/Binary/BinaryVersion.php b/src/Module/Binary/BinaryVersion.php index 75e80db..e4e44e3 100644 --- a/src/Module/Binary/BinaryVersion.php +++ b/src/Module/Binary/BinaryVersion.php @@ -21,7 +21,7 @@ final class BinaryVersion extends Version /** * Resolves the version from binary command output. * - * @param non-empty-string $output Output from binary execution + * @param string $output Output from binary execution * @return BinaryVersion Extracted version */ public static function fromBinaryOutput(string $output): static diff --git a/src/Module/Common/FileSystem/Path.php b/src/Module/Common/FileSystem/Path.php index d85719e..57447ef 100644 --- a/src/Module/Common/FileSystem/Path.php +++ b/src/Module/Common/FileSystem/Path.php @@ -71,6 +71,8 @@ public function name(): string /** * Return the file stem (the file name without its extension) + * + * @return non-empty-string */ public function stem(): string { diff --git a/src/Module/Common/Internal/ObjectContainer.php b/src/Module/Common/Internal/ObjectContainer.php index 1db6ee3..7d95962 100644 --- a/src/Module/Common/Internal/ObjectContainer.php +++ b/src/Module/Common/Internal/ObjectContainer.php @@ -24,7 +24,7 @@ final class ObjectContainer implements Container, ContainerInterface /** @var array */ private array $cache = []; - /** @var array */ + /** @var array */ private array $factory = []; private readonly Injector $injector; @@ -89,7 +89,7 @@ public function make(string $class, array $arguments = []): object /** * @template T * @param class-string $id Service identifier - * @param null|class-string|array|\Closure(Container): T $binding + * @param null|class-string|array|\Closure(mixed ...): T $binding */ public function bind(string $id, \Closure|string|array|null $binding = null): void { @@ -113,7 +113,7 @@ public function bind(string $id, \Closure|string|array|null $binding = null): vo "Class `$id` must have a factory or be a factory itself and implement `Factoriable`.", ); - /** @var T $object */ + /** @var \Closure(mixed ...): T $object */ $object = $id::create(...); $this->factory[$id] = $object; } diff --git a/src/Module/Task/Manager.php b/src/Module/Task/Manager.php index a1a69e3..2be8c98 100644 --- a/src/Module/Task/Manager.php +++ b/src/Module/Task/Manager.php @@ -31,7 +31,7 @@ */ final class Manager { - /** @var array{\Fiber, Deferred} Active fiber tasks */ + /** @var array Active fiber tasks */ private array $tasks = []; /** diff --git a/src/Module/Velox/Internal/DependencyChecker.php b/src/Module/Velox/Internal/DependencyChecker.php index 69e37ed..6251b75 100644 --- a/src/Module/Velox/Internal/DependencyChecker.php +++ b/src/Module/Velox/Internal/DependencyChecker.php @@ -28,6 +28,8 @@ * Checks for the availability of required binaries like Go and Velox, * and validates their versions against configured constraints. * + * @psalm-suppress PropertyNotSetInConstructor + * * @internal */ final class DependencyChecker @@ -358,13 +360,14 @@ private function downloadVelox(Software $software, array $downloads, Download $f } } - if ($usedFallback) { - return null; // No valid binary found after trying all downloads + /** @psalm-suppress RedundantCondition */ + if (!$usedFallback) { + # If no downloads were successful, use the fallback action + $usedFallback = true; + $downloads = [$fallbackAction]; + goto try_download; } - # If no downloads were successful, use the fallback action - $usedFallback = true; - $downloads = [$fallbackAction]; - goto try_download; + return null; // No valid binary found after trying all downloads } } diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php index 8134d20..ac318b0 100644 --- a/src/Module/Velox/Internal/VeloxBuilder.php +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -5,11 +5,13 @@ namespace Internal\DLoad\Module\Velox\Internal; use Internal\DLoad\Module\Binary\Binary; +use Internal\DLoad\Module\Binary\BinaryProvider; use Internal\DLoad\Module\Common\FileSystem\FS; use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Common\OperatingSystem; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; use Internal\DLoad\Module\Config\Schema\Downloader; +use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; use Internal\DLoad\Module\Velox\Builder; use Internal\DLoad\Module\Velox\Exception\Build as BuildException; use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; @@ -38,18 +40,21 @@ public function __construct( private readonly Logger $logger, private readonly Downloader $appConfig, private readonly OperatingSystem $operatingSystem, + private readonly BinaryProvider $binaryProvider, ) {} public function build(VeloxAction $config, \Closure $onProgress): Task { $handler = function () use ($config, $onProgress): PromiseInterface { - # Prepare the destination binary path - $destination = Path::create($config->binaryPath ?? 'rr')->absolute(); - $destination->extension() !== $this->operatingSystem->getBinaryExtension() and $destination = $destination - ->parent() - ->join($destination->stem() . $this->operatingSystem->getBinaryExtension()); - try { + // $this->validate($config); + + # Prepare the destination binary path + $destination = Path::create($config->binaryPath ?? 'rr')->absolute(); + $destination->extension() !== $this->operatingSystem->getBinaryExtension() and $destination = $destination + ->parent() + ->join($destination->stem() . $this->operatingSystem->getBinaryExtension()); + # Prepare environment # Create build directory $buildDir = FS::tmpDir($this->appConfig->tmpDir, 'velox-build'); @@ -68,19 +73,16 @@ public function build(VeloxAction $config, \Closure $onProgress): Task # Build # Execute build command - $builtBinary = $this->executeBuild($configPath, $buildDir, $vxBinary); + $builtPath = $this->executeBuild($configPath, $buildDir, $vxBinary); # Move built binary to destination - $this->installBinary($builtBinary, $destination); + $binary = $this->installBinary($builtPath, $destination); - $version = Version::fromVersionString($config->binaryVersion); return resolve(new Result( - binaryPath: $destination, - version: $version, + binary: $binary, metadata: [ - 'config_file' => $config->configFile, 'velox_version' => $vxBinary->getVersion(), 'golang_version' => $goBinary->getVersion(), - 'binary_version' => $version, + 'build_config' => $config, ], )); } catch (\Throwable $e) { @@ -94,6 +96,18 @@ public function build(VeloxAction $config, \Closure $onProgress): Task return new Task($config, $onProgress, $handler, $this->getBuildName($config)); } + public function validate(VeloxAction $config): void + { + ConfigValidator::validate($config); + + // For this basic implementation, only local config files are supported + if ($config->configFile === null) { + throw new ConfigException( + 'This implementation only supports local config files. Remote API configuration is not yet implemented.', + ); + } + } + private function prepareConfig(VeloxAction $config, Path $buildDir): Path { $sourceConfig = Path::create($config->configFile ?? 'velox.toml'); @@ -177,7 +191,7 @@ private function findBuiltBinary(Path $buildDir): ?Path * * @throws \RuntimeException If the destination cannot be created or the binary cannot be moved */ - private function installBinary(Path $builtBinary, Path $destination): void + private function installBinary(Path $builtBinary, Path $destination): Binary { # Check if build binary already exists $destination->exists() @@ -190,6 +204,13 @@ private function installBinary(Path $builtBinary, Path $destination): void \chmod($destination->__toString(), 0755); $this->logger->info('Installed binary to: %s', $destination->__toString()); + + $binaryConfig = new BinaryConfig(); + $binaryConfig->versionCommand = '--version'; + $binaryConfig->name = $destination->stem(); + return $this->binaryProvider->getLocalBinary($destination->parent(), $binaryConfig) ?? throw new \RuntimeException( + "Failed to create binary instance for: {$destination}", + ); } /** diff --git a/src/Module/Velox/Result.php b/src/Module/Velox/Result.php index 91163d5..e71b6b7 100644 --- a/src/Module/Velox/Result.php +++ b/src/Module/Velox/Result.php @@ -4,8 +4,7 @@ namespace Internal\DLoad\Module\Velox; -use Internal\DLoad\Module\Common\FileSystem\Path; -use Internal\DLoad\Module\Version\Version; +use Internal\DLoad\Module\Binary\Binary; /** * Represents the result of a successful build operation. @@ -20,61 +19,11 @@ final class Result /** * Creates a new build result. * - * @param Path $binaryPath Path to the built binary - * @param Version $version Version of the built software - * @param array $metadata Additional build metadata - * @param int $buildDuration Build time in seconds - * @param list $artifacts Additional artifacts created during build + * @param Binary $binary The built binary + * @param array $metadata Additional build metadata */ public function __construct( - public readonly Path $binaryPath, - public readonly Version $version, + public readonly Binary $binary, public readonly array $metadata = [], - public readonly int $buildDuration = 0, - public readonly array $artifacts = [], ) {} - - /** - * Checks if the built binary exists and is executable. - * - * @return bool True if binary is valid and executable - */ - public function isValid(): bool - { - return $this->binaryPath->exists() - && $this->binaryPath->isFile() - && \is_executable((string) $this->binaryPath); - } - - /** - * Returns the size of the built binary in bytes. - * - * @return int|null Binary size or null if file doesn't exist - */ - public function getBinarySize(): ?int - { - if (!$this->binaryPath->exists()) { - return null; - } - return \filesize((string) $this->binaryPath) ?: null; - } - - /** - * Returns build metadata as a formatted string. - * - * @return string Human-readable build information - */ - public function getSummary(): string - { - $size = $this->getBinarySize(); - $sizeStr = $size !== null ? \sprintf('%.2f MB', $size / 1024 / 1024) : 'unknown'; - - return \sprintf( - 'Built %s v%s (%s, %ds)', - $this->binaryPath->name(), - $this->version, - $sizeStr, - $this->buildDuration, - ); - } } diff --git a/src/Module/Velox/Task.php b/src/Module/Velox/Task.php index 0777e13..cd80fb1 100644 --- a/src/Module/Velox/Task.php +++ b/src/Module/Velox/Task.php @@ -72,7 +72,7 @@ public function getId(): string public function getDescription(): string { $pluginCount = \count($this->config->plugins); - $configSource = $this->config->configFile ? 'local config' : 'API config'; + $configSource = $this->config->configFile === null ? 'API config' : 'local config'; return \sprintf( 'Build RoadRunner with %d plugins using %s', From 2491138316d1adc8c3e2f51421ba838d7c5fdc24 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 11 Jul 2025 14:00:42 +0000 Subject: [PATCH 23/38] style(php-cs-fixer): fix coding standards --- src/Command/Build.php | 1 - src/Module/Downloader/SoftwareCollection.php | 3 +-- src/Module/Velox/Internal/VeloxBuilder.php | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Command/Build.php b/src/Command/Build.php index bf8ad26..3d72474 100644 --- a/src/Command/Build.php +++ b/src/Command/Build.php @@ -4,7 +4,6 @@ namespace Internal\DLoad\Command; -use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; use Internal\DLoad\Module\Config\Schema\Actions; use Internal\DLoad\Module\Task\Progress; diff --git a/src/Module/Downloader/SoftwareCollection.php b/src/Module/Downloader/SoftwareCollection.php index e322fcb..f5c7ea9 100644 --- a/src/Module/Downloader/SoftwareCollection.php +++ b/src/Module/Downloader/SoftwareCollection.php @@ -7,7 +7,6 @@ use Internal\DLoad\Info; use Internal\DLoad\Module\Config\Schema\CustomSoftwareRegistry; use Internal\DLoad\Module\Config\Schema\Embed\Software; -use IteratorAggregate; /** * Collection of software package configurations. @@ -15,7 +14,7 @@ * Manages both custom and default software registry entries. * Provides lookup functionality to find software by name or alias. * - * @implements IteratorAggregate + * @implements \IteratorAggregate */ final class SoftwareCollection implements \IteratorAggregate, \Countable { diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php index ac318b0..f263be8 100644 --- a/src/Module/Velox/Internal/VeloxBuilder.php +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -17,7 +17,6 @@ use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; use Internal\DLoad\Module\Velox\Result; use Internal\DLoad\Module\Velox\Task; -use Internal\DLoad\Module\Version\Version; use Internal\DLoad\Service\Logger; use React\Promise\PromiseInterface; From f81b110cb1e7c471547f32173618c8adc287f196 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 12 Jul 2025 01:13:17 +0400 Subject: [PATCH 24/38] docs(readme): update README files with new sections for building custom RoadRunner and Velox configuration --- README-es.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++- README-ru.md | 158 +++++++++++++++++++++++++++++++++++++++++++++++++- README-zh.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 621 insertions(+), 8 deletions(-) diff --git a/README-es.md b/README-es.md index 7c71d1f..c8bd58d 100644 --- a/README-es.md +++ b/README-es.md @@ -32,6 +32,43 @@ Con DLoad, puedes: - Gestionar compatibilidad multiplataforma sin configuración manual - Mantener binarios y recursos separados de tu control de versiones +### Tabla de Contenidos + +- [Instalación](#instalación) +- [Inicio Rápido](#inicio-rápido) +- [Uso de Línea de Comandos](#uso-de-línea-de-comandos) + - [Inicializar Configuración](#inicializar-configuración) + - [Descargar Software](#descargar-software) + - [Ver Software](#ver-software) + - [Construir Software Personalizado](#construir-software-personalizado) +- [Guía de Configuración](#guía-de-configuración) + - [Configuración Interactiva](#configuración-interactiva) + - [Configuración Manual](#configuración-manual) + - [Tipos de Descarga](#tipos-de-descarga) + - [Restricciones de Versión](#restricciones-de-versión) + - [Opciones de Configuración Avanzadas](#opciones-de-configuración-avanzadas) +- [Construir RoadRunner Personalizado](#construir-roadrunner-personalizado) + - [Configuración de Acción de Construcción](#configuración-de-acción-de-construcción) + - [Atributos de Acción Velox](#atributos-de-acción-velox) + - [Proceso de Construcción](#proceso-de-construcción) + - [Generación de Archivo de Configuración](#generación-de-archivo-de-configuración) + - [Usando Velox Descargado](#usando-velox-descargado) + - [Configuración DLoad](#configuración-dload) + - [Construyendo RoadRunner](#construyendo-roadrunner) +- [Registro de Software Personalizado](#registro-de-software-personalizado) + - [Definiendo Software](#definiendo-software) + - [Elementos de Software](#elementos-de-software) +- [Casos de Uso](#casos-de-uso) + - [Configuración de Entorno de Desarrollo](#configuración-de-entorno-de-desarrollo) + - [Configuración de Nuevo Proyecto](#configuración-de-nuevo-proyecto) + - [Integración CI/CD](#integración-cicd) + - [Equipos Multiplataforma](#equipos-multiplataforma) + - [Gestión de Herramientas PHAR](#gestión-de-herramientas-phar) + - [Distribución de Recursos Frontend](#distribución-de-recursos-frontend) +- [Límites de Rate de API de GitHub](#límites-de-rate-de-api-de-github) +- [Contribuciones](#contribuciones) + + ## Instalación ```bash @@ -51,6 +88,8 @@ composer require internal/dload -W composer require internal/dload -W ``` +Alternativamente, puedes descargar la última versión desde [GitHub releases](https://github.com/php-internal/dload/releases). + 2. **Crea tu archivo de configuración interactivamente**: ```bash @@ -62,7 +101,8 @@ composer require internal/dload -W ```xml + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/php-internal/dload/refs/heads/1.x/dload.xsd" + > @@ -81,7 +121,7 @@ composer require internal/dload -W ```json { "scripts": { - "post-update-cmd": "dload get --no-interaction -v || echo can't dload binaries" + "post-update-cmd": "dload get --no-interaction -v || \"echo can't dload binaries\"" } } ``` @@ -144,6 +184,25 @@ composer require internal/dload -W ./vendor/bin/dload show --all ``` +### Construir Software Personalizado + +```bash +# Construir software personalizado usando archivo de configuración +./vendor/bin/dload build + +# Construir con archivo de configuración específico +./vendor/bin/dload build --config=./custom-dload.xml +``` + +#### Opciones de Construcción + +| Opción | Descripción | Por defecto | +|--------|-------------|---------| +| `--config` | Ruta al archivo de configuración | ./dload.xml | + +El comando `build` ejecuta acciones de construcción definidas en tu archivo de configuración, como crear binarios RoadRunner personalizados con plugins específicos. +Para información detallada sobre construir RoadRunner personalizado, consulta la sección [Construir RoadRunner Personalizado](#construir-roadrunner-personalizado). + ## Guía de Configuración ### Configuración Interactiva @@ -264,6 +323,101 @@ Usa restricciones de versión estilo Composer: ``` +## Construir RoadRunner Personalizado + +DLoad soporta construir binarios RoadRunner personalizados usando la herramienta de construcción Velox. Esto es útil cuando necesitas RoadRunner con combinaciones de plugins personalizados que no están disponibles en versiones pre-construidas. + +### Configuración de Acción de Construcción + +```xml + + + + + + + +``` + +### Atributos de Acción Velox + +| Atributo | Descripción | Por defecto | +|-----------|-------------|---------| +| `velox-version` | Versión de la herramienta de construcción Velox | Última | +| `golang-version` | Versión de Go requerida | Última | +| `binary-version` | Versión de RoadRunner para mostrar en `rr --version` | Última | +| `config-file` | Ruta al archivo velox.toml local | `./velox.toml` | +| `binary-path` | Ruta para guardar el binario RoadRunner construido | `./rr` | + +### Proceso de Construcción + +DLoad maneja automáticamente el proceso de construcción: + +1. **Verificación de Golang**: Verifica que Go esté instalado globalmente (dependencia requerida) +2. **Preparación de Velox**: Usa Velox desde instalación global, descarga local, o descarga automáticamente si es necesario +3. **Configuración**: Copia tu velox.toml local al directorio de construcción +4. **Construcción**: Ejecuta el comando `vx build` con la configuración especificada +5. **Instalación**: Mueve el binario construido a la ubicación objetivo y establece permisos de ejecución +6. **Limpieza**: Elimina archivos temporales de construcción + +> [!NOTE] +> DLoad requiere que Go (Golang) esté instalado globalmente en tu sistema. No descarga ni gestiona instalaciones de Go. + +### Generación de Archivo de Configuración + +Puedes generar un archivo de configuración `velox.toml` usando el constructor en línea en https://build.roadrunner.dev/ + +Para documentación detallada sobre opciones de configuración de Velox y ejemplos, visita https://docs.roadrunner.dev/docs/customization/build + +Esta interfaz web te ayuda a seleccionar plugins y genera la configuración apropiada para tu construcción RoadRunner personalizada. + +### Usando Velox Descargado + +Puedes descargar Velox como parte de tu proceso de construcción en lugar de depender de una versión instalada globalmente: + +```xml + + + + +``` + +Esto asegura versiones consistentes de Velox entre diferentes entornos y miembros del equipo. + +### Configuración DLoad + +```xml + + + + + + +``` + +### Construyendo RoadRunner + +```bash +# Construir RoadRunner usando configuración velox.toml +./vendor/bin/dload build + +# Construir con archivo de configuración específico +./vendor/bin/dload build --config=custom-rr.xml +``` + +El binario RoadRunner construido incluirá solo los plugins especificados en tu archivo `velox.toml`, reduciendo el tamaño del binario y mejorando el rendimiento para tu caso de uso específico. + ## Registro de Software Personalizado ### Definiendo Software @@ -404,5 +558,5 @@ Añádelo a variables de entorno CI/CD para descargas automatizadas. ¡Las contribuciones son bienvenidas! Envía Pull Requests para: - Añadir nuevo software al registro predefinido -- Mejorar la funcionalidad de DLoad +- Mejorar la funcionalidad de DLoad - Mejorar la documentación y traducirla a [otros idiomas](docs/guidelines/how-to-translate-readme-docs.md) diff --git a/README-ru.md b/README-ru.md index b4868aa..4ba4241 100644 --- a/README-ru.md +++ b/README-ru.md @@ -32,6 +32,43 @@ DLoad решает общую проблему в PHP-проектах: как - Управлять кроссплатформенной совместимостью без ручной конфигурации - Хранить бинарные файлы и ресурсы отдельно от системы контроля версий +### Содержание + +- [Установка](#установка) +- [Быстрый старт](#быстрый-старт) +- [Использование командной строки](#использование-командной-строки) + - [Инициализация конфигурации](#инициализация-конфигурации) + - [Загрузка ПО](#загрузка-по) + - [Просмотр ПО](#просмотр-по) + - [Сборка пользовательского ПО](#сборка-пользовательского-по) +- [Руководство по конфигурации](#руководство-по-конфигурации) + - [Интерактивная конфигурация](#интерактивная-конфигурация) + - [Ручная конфигурация](#ручная-конфигурация) + - [Типы загрузки](#типы-загрузки) + - [Ограничения версий](#ограничения-версий) + - [Расширенные опции конфигурации](#расширенные-опции-конфигурации) +- [Сборка пользовательского RoadRunner](#сборка-пользовательского-roadrunner) + - [Конфигурация действия сборки](#конфигурация-действия-сборки) + - [Атрибуты действия Velox](#атрибуты-действия-velox) + - [Процесс сборки](#процесс-сборки) + - [Генерация файла конфигурации](#генерация-файла-конфигурации) + - [Использование загруженного Velox](#использование-загруженного-velox) + - [Конфигурация DLoad](#конфигурация-dload) + - [Сборка RoadRunner](#сборка-roadrunner) +- [Пользовательский реестр ПО](#пользовательский-реестр-по) + - [Определение ПО](#определение-по) + - [Элементы ПО](#элементы-по) +- [Случаи использования](#случаи-использования) + - [Настройка среды разработки](#настройка-среды-разработки) + - [Настройка нового проекта](#настройка-нового-проекта) + - [Интеграция CI/CD](#интеграция-cicd) + - [Кроссплатформенные команды](#кроссплатформенные-команды) + - [Управление PHAR инструментами](#управление-phar-инструментами) + - [Распространение фронтенд ресурсов](#распространение-фронтенд-ресурсов) +- [Ограничения API GitHub](#ограничения-api-github) +- [Вклад в проект](#вклад-в-проект) + + ## Установка ```bash @@ -51,6 +88,8 @@ composer require internal/dload -W composer require internal/dload -W ``` +Альтернативно, вы можете скачать последний релиз с [GitHub releases](https://github.com/php-internal/dload/releases). + 2. **Создайте файл конфигурации интерактивно**: ```bash @@ -62,7 +101,8 @@ composer require internal/dload -W ```xml + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/php-internal/dload/refs/heads/1.x/dload.xsd" + > @@ -81,7 +121,7 @@ composer require internal/dload -W ```json { "scripts": { - "post-update-cmd": "dload get --no-interaction -v || echo can't dload binaries" + "post-update-cmd": "dload get --no-interaction -v || \"echo can't dload binaries\"" } } ``` @@ -144,6 +184,25 @@ composer require internal/dload -W ./vendor/bin/dload show --all ``` +### Сборка пользовательского ПО + +```bash +# Собрать пользовательское ПО, используя файл конфигурации +./vendor/bin/dload build + +# Собрать с определенным файлом конфигурации +./vendor/bin/dload build --config=./custom-dload.xml +``` + +#### Опции сборки + +| Опция | Описание | По умолчанию | +|--------|-------------|---------| +| `--config` | Путь к файлу конфигурации | ./dload.xml | + +Команда `build` выполняет действия сборки, определенные в вашем файле конфигурации, такие как создание пользовательских бинарных файлов RoadRunner с определенными плагинами. +Для подробной информации о сборке пользовательского RoadRunner смотрите раздел [Сборка пользовательского RoadRunner](#сборка-пользовательского-roadrunner). + ## Руководство по конфигурации ### Интерактивная конфигурация @@ -264,6 +323,101 @@ DLoad поддерживает три типа загрузки, которые ``` +## Сборка пользовательского RoadRunner + +DLoad поддерживает сборку пользовательских бинарных файлов RoadRunner с использованием инструмента сборки Velox. Это полезно, когда вам нужен RoadRunner с пользовательскими комбинациями плагинов, которые недоступны в готовых релизах. + +### Конфигурация действия сборки + +```xml + + + + + + + +``` + +### Атрибуты действия Velox + +| Атрибут | Описание | По умолчанию | +|-----------|-------------|---------| +| `velox-version` | Версия инструмента сборки Velox | Последняя | +| `golang-version` | Требуемая версия Go | Последняя | +| `binary-version` | Версия RoadRunner для отображения в `rr --version` | Последняя | +| `config-file` | Путь к локальному файлу velox.toml | `./velox.toml` | +| `binary-path` | Путь для сохранения собранного бинарного файла RoadRunner | `./rr` | + +### Процесс сборки + +DLoad автоматически обрабатывает процесс сборки: + +1. **Проверка Golang**: Проверяет, что Go установлен глобально (обязательная зависимость) +2. **Подготовка Velox**: Использует Velox из глобальной установки, локальной загрузки или автоматически загружает при необходимости +3. **Конфигурация**: Копирует ваш локальный velox.toml в директорию сборки +4. **Сборка**: Выполняет команду `vx build` с указанной конфигурацией +5. **Установка**: Перемещает собранный бинарный файл в целевое место и устанавливает права на выполнение +6. **Очистка**: Удаляет временные файлы сборки + +> [!NOTE] +> DLoad требует, чтобы Go (Golang) был установлен глобально в вашей системе. Он не загружает и не управляет установками Go. + +### Генерация файла конфигурации + +Вы можете сгенерировать файл конфигурации `velox.toml`, используя онлайн-конструктор на https://build.roadrunner.dev/ + +Для подробной документации по опциям конфигурации Velox и примерам посетите https://docs.roadrunner.dev/docs/customization/build + +Этот веб-интерфейс помогает выбрать плагины и создает соответствующую конфигурацию для вашей пользовательской сборки RoadRunner. + +### Использование загруженного Velox + +Вы можете загрузить Velox как часть процесса сборки вместо использования глобально установленной версии: + +```xml + + + + +``` + +Это обеспечивает согласованные версии Velox в разных окружениях и среди участников команды. + +### Конфигурация DLoad + +```xml + + + + + + +``` + +### Сборка RoadRunner + +```bash +# Собрать RoadRunner, используя конфигурацию velox.toml +./vendor/bin/dload build + +# Собрать с определенным файлом конфигурации +./vendor/bin/dload build --config=custom-rr.xml +``` + +Собранный бинарный файл RoadRunner будет включать только плагины, указанные в вашем файле `velox.toml`, уменьшая размер бинарного файла и улучшая производительность для вашего конкретного случая использования. + ## Пользовательский реестр ПО ### Определение ПО diff --git a/README-zh.md b/README-zh.md index e27cfb8..719cb11 100644 --- a/README-zh.md +++ b/README-zh.md @@ -32,6 +32,43 @@ DLoad 解决了 PHP 项目中的一个常见问题:如何在 PHP 代码的同 - 管理跨平台兼容性,无需手动配置 - 将二进制文件和资产与版本控制分开 +### 目录 + +- [安装](#安装) +- [快速开始](#快速开始) +- [命令行使用](#命令行使用) + - [初始化配置](#初始化配置) + - [下载软件](#下载软件) + - [查看软件](#查看软件) + - [构建自定义软件](#构建自定义软件) +- [配置指南](#配置指南) + - [交互式配置](#交互式配置) + - [手动配置](#手动配置) + - [下载类型](#下载类型) + - [版本约束](#版本约束) + - [高级配置选项](#高级配置选项) +- [构建自定义 RoadRunner](#构建自定义-roadrunner) + - [构建操作配置](#构建操作配置) + - [Velox 操作属性](#velox-操作属性) + - [构建过程](#构建过程) + - [配置文件生成](#配置文件生成) + - [使用下载的 Velox](#使用下载的-velox) + - [DLoad 配置](#dload-配置) + - [构建 RoadRunner](#构建-roadrunner) +- [自定义软件注册表](#自定义软件注册表) + - [定义软件](#定义软件) + - [软件元素](#软件元素) +- [用例](#用例) + - [开发环境设置](#开发环境设置) + - [新项目设置](#新项目设置) + - [CI/CD 集成](#cicd-集成) + - [跨平台团队](#跨平台团队) + - [PHAR 工具管理](#phar-工具管理) + - [前端资产分发](#前端资产分发) +- [GitHub API 速率限制](#github-api-速率限制) +- [贡献](#贡献) + + ## 安装 ```bash @@ -51,6 +88,8 @@ composer require internal/dload -W composer require internal/dload -W ``` +或者,您可以从 [GitHub 发布页面](https://github.com/php-internal/dload/releases) 下载最新版本。 + 2. **交互式创建配置文件**: ```bash @@ -62,7 +101,8 @@ composer require internal/dload -W ```xml + xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/php-internal/dload/refs/heads/1.x/dload.xsd" + > @@ -81,7 +121,7 @@ composer require internal/dload -W ```json { "scripts": { - "post-update-cmd": "dload get --no-interaction -v || echo can't dload binaries" + "post-update-cmd": "dload get --no-interaction -v || \"echo can't dload binaries\"" } } ``` @@ -144,6 +184,25 @@ composer require internal/dload -W ./vendor/bin/dload show --all ``` +### 构建自定义软件 + +```bash +# 使用配置文件构建自定义软件 +./vendor/bin/dload build + +# 使用特定配置文件构建 +./vendor/bin/dload build --config=./custom-dload.xml +``` + +#### 构建选项 + +| 选项 | 描述 | 默认值 | +|--------|-------------|---------| +| `--config` | 配置文件路径 | ./dload.xml | + +`build` 命令执行配置文件中定义的构建操作,例如创建具有特定插件的自定义 RoadRunner 二进制文件。 +有关构建自定义 RoadRunner 的详细信息,请参阅 [构建自定义 RoadRunner](#构建自定义-roadrunner) 部分。 + ## 配置指南 ### 交互式配置 @@ -264,6 +323,101 @@ DLoad 支持三种下载类型,决定资产的处理方式: ``` +## 构建自定义 RoadRunner + +DLoad 支持使用 Velox 构建工具构建自定义 RoadRunner 二进制文件。当您需要具有预构建版本中不可用的自定义插件组合的 RoadRunner 时,这非常有用。 + +### 构建操作配置 + +```xml + + + + + + + +``` + +### Velox 操作属性 + +| 属性 | 描述 | 默认值 | +|-----------|-------------|---------| +| `velox-version` | Velox 构建工具版本 | 最新 | +| `golang-version` | 所需的 Go 版本 | 最新 | +| `binary-version` | 在 `rr --version` 中显示的 RoadRunner 版本 | 最新 | +| `config-file` | 本地 velox.toml 文件路径 | `./velox.toml` | +| `binary-path` | 保存构建的 RoadRunner 二进制文件的路径 | `./rr` | + +### 构建过程 + +DLoad 自动处理构建过程: + +1. **Golang 检查**:验证 Go 是否全局安装(必需依赖项) +2. **Velox 准备**:使用全局安装的 Velox、本地下载或在需要时自动下载 +3. **配置**:将您的本地 velox.toml 复制到构建目录 +4. **构建**:使用指定配置执行 `vx build` 命令 +5. **安装**:将构建的二进制文件移动到目标位置并设置可执行权限 +6. **清理**:删除临时构建文件 + +> [!NOTE] +> DLoad 需要在您的系统上全局安装 Go (Golang)。它不会下载或管理 Go 安装。 + +### 配置文件生成 + +您可以使用 https://build.roadrunner.dev/ 上的在线构建器生成 `velox.toml` 配置文件 + +有关 Velox 配置选项和示例的详细文档,请访问 https://docs.roadrunner.dev/docs/customization/build + +此 Web 界面帮助您选择插件并为您的自定义 RoadRunner 构建生成适当的配置。 + +### 使用下载的 Velox + +您可以将 Velox 作为构建过程的一部分下载,而不是依赖全局安装的版本: + +```xml + + + + +``` + +这确保在不同环境和团队成员之间使用一致的 Velox 版本。 + +### DLoad 配置 + +```xml + + + + + + +``` + +### 构建 RoadRunner + +```bash +# 使用 velox.toml 配置构建 RoadRunner +./vendor/bin/dload build + +# 使用特定配置文件构建 +./vendor/bin/dload build --config=custom-rr.xml +``` + +构建的 RoadRunner 二进制文件将仅包含您在 `velox.toml` 文件中指定的插件,从而减少二进制文件大小并提高特定用例的性能。 + ## 自定义软件注册表 ### 定义软件 @@ -404,5 +558,5 @@ GITHUB_TOKEN=your_token_here ./vendor/bin/dload get 欢迎贡献!提交拉取请求以: - 向预定义注册表添加新软件 -- 改进 DLoad 功能 +- 改进 DLoad 功能 - 增强文档并将其翻译为[其他语言](docs/guidelines/how-to-translate-readme-docs.md) diff --git a/README.md b/README.md index 6a428c0..6e38d56 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,43 @@ With DLoad, you can: - Manage cross-platform compatibility without manual configuration - Keep binaries and assets separate from your version control +### Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Command Line Usage](#command-line-usage) + - [Initialize Configuration](#initialize-configuration) + - [Download Software](#download-software) + - [View Software](#view-software) + - [Build Custom Software](#build-custom-software) +- [Configuration Guide](#configuration-guide) + - [Interactive Configuration](#interactive-configuration) + - [Manual Configuration](#manual-configuration) + - [Download Types](#download-types) + - [Version Constraints](#version-constraints) + - [Advanced Configuration Options](#advanced-configuration-options) +- [Building Custom RoadRunner](#building-custom-roadrunner) + - [Build Action Configuration](#build-action-configuration) + - [Velox Action Attributes](#velox-action-attributes) + - [Build Process](#build-process) + - [Configuration File Generation](#configuration-file-generation) + - [Using Downloaded Velox](#using-downloaded-velox) + - [DLoad Configuration](#dload-configuration) + - [Building RoadRunner](#building-roadrunner) +- [Custom Software Registry](#custom-software-registry) + - [Defining Software](#defining-software) + - [Software Elements](#software-elements) +- [Use Cases](#use-cases) + - [Development Environment Setup](#development-environment-setup) + - [New Project Setup](#new-project-setup) + - [CI/CD Integration](#cicd-integration) + - [Cross-Platform Teams](#cross-platform-teams) + - [PHAR Tools Management](#phar-tools-management) + - [Frontend Asset Distribution](#frontend-asset-distribution) +- [GitHub API Rate Limits](#github-api-rate-limits) +- [Contributing](#contributing) + + ## Installation ```bash @@ -147,6 +184,25 @@ Alternatively, you can download the latest release from [GitHub releases](https: ./vendor/bin/dload show --all ``` +### Build Custom Software + +```bash +# Build custom software using configuration file +./vendor/bin/dload build + +# Build with specific configuration file +./vendor/bin/dload build --config=./custom-dload.xml +``` + +#### Build Options + +| Option | Description | Default | +|--------|-------------|---------| +| `--config` | Path to configuration file | ./dload.xml | + +The `build` command executes build actions defined in your configuration file, such as creating custom RoadRunner binaries with specific plugins. +For detailed information about building custom RoadRunner, see the [Building Custom RoadRunner](#building-custom-roadrunner) section. + ## Configuration Guide ### Interactive Configuration @@ -267,6 +323,101 @@ Use Composer-style version constraints: ``` +## Building Custom RoadRunner + +DLoad supports building custom RoadRunner binaries using the Velox build tool. This is useful when you need RoadRunner with custom plugin combinations that aren't available in pre-built releases. + +### Build Action Configuration + +```xml + + + + + + + +``` + +### Velox Action Attributes + +| Attribute | Description | Default | +|-----------|-------------|---------| +| `velox-version` | Version of Velox build tool | Latest | +| `golang-version` | Required Go version | Latest | +| `binary-version` | RoadRunner version to display in `rr --version` | Latest | +| `config-file` | Path to local velox.toml file | `./velox.toml` | +| `binary-path` | Path to save the built RoadRunner binary | `./rr` | + +### Build Process + +DLoad automatically handles the build process: + +1. **Golang Check**: Verifies Go is installed globally (required dependency) +2. **Velox Preparation**: Uses Velox from global installation, local download, or downloads automatically if needed +3. **Configuration**: Copies your local velox.toml to build directory +4. **Building**: Executes `vx build` command with specified configuration +5. **Installation**: Moves built binary to target location and sets executable permissions +6. **Cleanup**: Removes temporary build files + +> [!NOTE] +> DLoad requires Go (Golang) to be installed globally on your system. It does not download or manage Go installations. + +### Configuration File Generation + +You can generate a `velox.toml` configuration file using the online builder at https://build.roadrunner.dev/ + +For detailed documentation on Velox configuration options and examples, visit https://docs.roadrunner.dev/docs/customization/build + +This web interface helps you select plugins and generates the appropriate configuration for your custom RoadRunner build. + +### Using Downloaded Velox + +You can download Velox as part of your build process instead of relying on a globally installed version: + +```xml + + + + +``` + +This ensures consistent Velox versions across different environments and team members. + +### DLoad Configuration + +```xml + + + + + + +``` + +### Building RoadRunner + +```bash +# Build RoadRunner using velox.toml configuration +./vendor/bin/dload build + +# Build with specific configuration file +./vendor/bin/dload build --config=custom-rr.xml +``` + +The built RoadRunner binary will include only the plugins specified in your `velox.toml` file, reducing binary size and improving performance for your specific use case. + ## Custom Software Registry ### Defining Software From f91673959b585fb67ad50e39d39bdedb777fa2ef Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 12 Jul 2025 19:24:20 +0400 Subject: [PATCH 25/38] docs(readme): improve Spanish and Russian README translations for clarity and consistency --- README-es.md | 200 +++++++------- README-ru.md | 257 +++++++++--------- README-zh.md | 222 +++++++-------- .../how-to-translate-readme-docs.md | 2 + 4 files changed, 342 insertions(+), 339 deletions(-) diff --git a/README-es.md b/README-es.md index c8bd58d..6ed7828 100644 --- a/README-es.md +++ b/README-es.md @@ -14,7 +14,7 @@
-DLoad simplifica la descarga y gestión de artefactos binarios para tus proyectos. Perfecto para entornos de desarrollo que requieren herramientas específicas como RoadRunner, Temporal o binarios personalizados. +DLoad simplifica la descarga y gestión de artefactos binarios para tus proyectos. Es perfecto para entornos de desarrollo que necesitan herramientas específicas como RoadRunner, Temporal o binarios personalizados. [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%BA%F0%9F%87%B8-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) @@ -24,19 +24,19 @@ DLoad simplifica la descarga y gestión de artefactos binarios para tus proyecto ## ¿Por qué DLoad? DLoad resuelve un problema común en proyectos PHP: cómo distribuir e instalar herramientas binarias y recursos necesarios junto con tu código PHP. -Con DLoad, puedes: +Con DLoad puedes: -- Descargar automáticamente herramientas requeridas durante la inicialización del proyecto -- Asegurar que todos los miembros del equipo usen las mismas versiones de herramientas -- Simplificar la incorporación automatizando la configuración del entorno -- Gestionar compatibilidad multiplataforma sin configuración manual -- Mantener binarios y recursos separados de tu control de versiones +- Descargar automáticamente las herramientas que necesitas durante la configuración inicial del proyecto +- Asegurar que todo el equipo use exactamente las mismas versiones de las herramientas +- Simplificar la incorporación de nuevos desarrolladores automatizando la configuración del entorno +- Manejar compatibilidad multiplataforma sin configuración manual +- Mantener binarios y recursos fuera de tu control de versiones ### Tabla de Contenidos - [Instalación](#instalación) - [Inicio Rápido](#inicio-rápido) -- [Uso de Línea de Comandos](#uso-de-línea-de-comandos) +- [Uso desde Línea de Comandos](#uso-desde-línea-de-comandos) - [Inicializar Configuración](#inicializar-configuración) - [Descargar Software](#descargar-software) - [Ver Software](#ver-software) @@ -52,21 +52,21 @@ Con DLoad, puedes: - [Atributos de Acción Velox](#atributos-de-acción-velox) - [Proceso de Construcción](#proceso-de-construcción) - [Generación de Archivo de Configuración](#generación-de-archivo-de-configuración) - - [Usando Velox Descargado](#usando-velox-descargado) + - [Usar Velox Descargado](#usar-velox-descargado) - [Configuración DLoad](#configuración-dload) - - [Construyendo RoadRunner](#construyendo-roadrunner) + - [Construir RoadRunner](#construir-roadrunner) - [Registro de Software Personalizado](#registro-de-software-personalizado) - - [Definiendo Software](#definiendo-software) + - [Definir Software](#definir-software) - [Elementos de Software](#elementos-de-software) - [Casos de Uso](#casos-de-uso) - - [Configuración de Entorno de Desarrollo](#configuración-de-entorno-de-desarrollo) - - [Configuración de Nuevo Proyecto](#configuración-de-nuevo-proyecto) + - [Configurar Entorno de Desarrollo](#configurar-entorno-de-desarrollo) + - [Configurar Nuevo Proyecto](#configurar-nuevo-proyecto) - [Integración CI/CD](#integración-cicd) - [Equipos Multiplataforma](#equipos-multiplataforma) - [Gestión de Herramientas PHAR](#gestión-de-herramientas-phar) - - [Distribución de Recursos Frontend](#distribución-de-recursos-frontend) -- [Límites de Rate de API de GitHub](#límites-de-rate-de-api-de-github) -- [Contribuciones](#contribuciones) + - [Distribución de Assets Frontend](#distribución-de-assets-frontend) +- [Límites de Rate de la API de GitHub](#límites-de-rate-de-la-api-de-github) +- [Contribuir](#contribuir) ## Instalación @@ -82,21 +82,21 @@ composer require internal/dload -W ## Inicio Rápido -1. **Instala DLoad vía Composer**: +1. **Instala DLoad usando Composer**: ```bash composer require internal/dload -W ``` -Alternativamente, puedes descargar la última versión desde [GitHub releases](https://github.com/php-internal/dload/releases). +También puedes descargar la versión más reciente desde [GitHub releases](https://github.com/php-internal/dload/releases). -2. **Crea tu archivo de configuración interactivamente**: +2. **Crea tu archivo de configuración de forma interactiva**: ```bash ./vendor/bin/dload init ``` - Este comando te guiará a través de la selección de paquetes de software y creará un archivo de configuración `dload.xml`. También puedes crearlo manualmente: + Este comando te ayudará a seleccionar paquetes de software y creará un archivo de configuración `dload.xml`. También puedes crearlo manualmente: ```xml @@ -116,7 +116,7 @@ Alternativamente, puedes descargar la última versión desde [GitHub releases](h ./vendor/bin/dload get ``` -4. **Integración con Composer** (opcional): +4. **Integra con Composer** (opcional): ```json { @@ -126,18 +126,18 @@ Alternativamente, puedes descargar la última versión desde [GitHub releases](h } ``` -## Uso de Línea de Comandos +## Uso desde Línea de Comandos ### Inicializar Configuración ```bash -# Crear archivo de configuración interactivamente +# Crear archivo de configuración de forma interactiva ./vendor/bin/dload init -# Crear configuración en ubicación específica +# Crear configuración en una ubicación específica ./vendor/bin/dload init --config=./custom-dload.xml -# Crear configuración mínima sin prompts +# Crear configuración mínima sin preguntas ./vendor/bin/dload init --no-interaction # Sobrescribir configuración existente sin confirmación @@ -147,26 +147,26 @@ Alternativamente, puedes descargar la última versión desde [GitHub releases](h ### Descargar Software ```bash -# Descargar desde archivo de configuración +# Descargar usando el archivo de configuración ./vendor/bin/dload get # Descargar paquetes específicos ./vendor/bin/dload get rr temporal -# Descargar con opciones +# Descargar con opciones adicionales ./vendor/bin/dload get rr --stability=beta --force ``` #### Opciones de Descarga -| Opción | Descripción | Por defecto | -|--------|-------------|---------| -| `--path` | Directorio para almacenar binarios | Directorio actual | -| `--arch` | Arquitectura objetivo (amd64, arm64) | Arquitectura del sistema | -| `--os` | SO objetivo (linux, darwin, windows) | SO actual | -| `--stability` | Estabilidad de lanzamiento (stable, beta) | stable | +| Opción | Descripción | Valor por defecto | +|--------|-------------|-------------------| +| `--path` | Directorio donde guardar los binarios | Directorio actual | +| `--arch` | Arquitectura de destino (amd64, arm64) | Arquitectura del sistema | +| `--os` | Sistema operativo de destino (linux, darwin, windows) | SO actual | +| `--stability` | Estabilidad del release (stable, beta) | stable | | `--config` | Ruta al archivo de configuración | ./dload.xml | -| `--force`, `-f` | Forzar descarga aunque el binario exista | false | +| `--force`, `-f` | Forzar descarga aunque el binario ya exista | false | ### Ver Software @@ -187,37 +187,37 @@ Alternativamente, puedes descargar la última versión desde [GitHub releases](h ### Construir Software Personalizado ```bash -# Construir software personalizado usando archivo de configuración +# Construir software personalizado usando el archivo de configuración ./vendor/bin/dload build -# Construir con archivo de configuración específico +# Construir con un archivo de configuración específico ./vendor/bin/dload build --config=./custom-dload.xml ``` #### Opciones de Construcción -| Opción | Descripción | Por defecto | -|--------|-------------|---------| +| Opción | Descripción | Valor por defecto | +|--------|-------------|-------------------| | `--config` | Ruta al archivo de configuración | ./dload.xml | -El comando `build` ejecuta acciones de construcción definidas en tu archivo de configuración, como crear binarios RoadRunner personalizados con plugins específicos. -Para información detallada sobre construir RoadRunner personalizado, consulta la sección [Construir RoadRunner Personalizado](#construir-roadrunner-personalizado). +El comando `build` ejecuta las acciones de construcción definidas en tu archivo de configuración, como crear binarios personalizados de RoadRunner con plugins específicos. +Para información detallada sobre cómo construir RoadRunner personalizado, consulta la sección [Construir RoadRunner Personalizado](#construir-roadrunner-personalizado). ## Guía de Configuración ### Configuración Interactiva -La forma más fácil de crear un archivo de configuración es usando el comando interactivo `init`: +La forma más sencilla de crear un archivo de configuración es usando el comando interactivo `init`: ```bash ./vendor/bin/dload init ``` -Esto: +Esto hará lo siguiente: -- Te guiará a través de la selección de paquetes de software +- Te guiará en la selección de paquetes de software - Mostrará software disponible con descripciones y repositorios -- Generará un archivo `dload.xml` correctamente formateado con validación de esquema +- Generará un archivo `dload.xml` bien formateado con validación de esquema - Manejará archivos de configuración existentes de manera elegante ### Configuración Manual @@ -239,31 +239,31 @@ Crea `dload.xml` en la raíz de tu proyecto: ### Tipos de Descarga -DLoad soporta tres tipos de descarga que determinan cómo se procesan los recursos: +DLoad soporta tres tipos de descarga que determinan cómo se procesan los assets: -#### Atributo de Tipo +#### Atributo Type ```xml - + - + - + ``` -#### Comportamiento Por Defecto (Tipo No Especificado) +#### Comportamiento por Defecto (Sin Especificar Type) -Cuando `type` no se especifica, DLoad automáticamente usa todos los manejadores disponibles: +Cuando no se especifica `type`, DLoad automáticamente usa todos los manejadores disponibles: -- **Procesamiento de binarios**: Si el software tiene sección ``, realiza verificación de presencia y versión de binarios -- **Procesamiento de archivos**: Si el software tiene sección `` y el recurso se descarga, procesa archivos durante el desempaquetado -- **Descarga simple**: Si no existen secciones, descarga el recurso sin desempaquetar +- **Procesamiento de binarios**: Si el software tiene una sección ``, verifica la presencia y versión del binario +- **Procesamiento de archivos**: Si el software tiene una sección `` y el asset se descarga, procesa los archivos durante la extracción +- **Descarga simple**: Si no hay secciones, descarga el asset sin extraer ```xml - + @@ -277,14 +277,14 @@ Cuando `type` no se especifica, DLoad automáticamente usa todos los manejadores #### Comportamientos de Tipos Explícitos | Tipo | Comportamiento | Caso de Uso | -|-----------|--------------------------------------------------------------|--------------------------------| +|-----------|-------------------------------------------------------------------|----------------------------------| | `binary` | Verificación de binarios, validación de versión, permisos de ejecución | Herramientas CLI, ejecutables | -| `phar` | Descarga archivos `.phar` como ejecutables **sin desempaquetar** | Herramientas PHP como Psalm, PHPStan | -| `archive` | **Fuerza desempaquetado incluso para archivos .phar** | Cuando necesitas contenido de archivo | +| `phar` | Descarga archivos `.phar` como ejecutables **sin extraer** | Herramientas PHP como Psalm, PHPStan | +| `archive` | **Fuerza la extracción incluso para archivos .phar** | Cuando necesitas el contenido del archivo | > [!NOTE] -> Usa `type="phar"` para herramientas PHP que deben permanecer como archivos `.phar`. -> Usar `type="archive"` desempaquetará incluso archivos `.phar`. +> Usa `type="phar"` para herramientas PHP que deben mantenerse como archivos `.phar`. +> Usar `type="archive"` extraerá incluso archivos `.phar`. ### Restricciones de Versión @@ -302,7 +302,7 @@ Usa restricciones de versión estilo Composer: - + ``` @@ -316,7 +316,7 @@ Usa restricciones de versión estilo Composer: - + @@ -325,7 +325,7 @@ Usa restricciones de versión estilo Composer: ## Construir RoadRunner Personalizado -DLoad soporta construir binarios RoadRunner personalizados usando la herramienta de construcción Velox. Esto es útil cuando necesitas RoadRunner con combinaciones de plugins personalizados que no están disponibles en versiones pre-construidas. +DLoad soporta la construcción de binarios personalizados de RoadRunner usando la herramienta Velox. Esto es útil cuando necesitas RoadRunner con combinaciones específicas de plugins que no están disponibles en las versiones pre-construidas. ### Configuración de Acción de Construcción @@ -345,23 +345,23 @@ DLoad soporta construir binarios RoadRunner personalizados usando la herramienta ### Atributos de Acción Velox -| Atributo | Descripción | Por defecto | -|-----------|-------------|---------| -| `velox-version` | Versión de la herramienta de construcción Velox | Última | -| `golang-version` | Versión de Go requerida | Última | +| Atributo | Descripción | Valor por defecto | +|-----------|-------------|-------------------| +| `velox-version` | Versión de la herramienta Velox | Última | +| `golang-version` | Versión requerida de Go | Última | | `binary-version` | Versión de RoadRunner para mostrar en `rr --version` | Última | | `config-file` | Ruta al archivo velox.toml local | `./velox.toml` | -| `binary-path` | Ruta para guardar el binario RoadRunner construido | `./rr` | +| `binary-path` | Ruta donde guardar el binario construido de RoadRunner | `./rr` | ### Proceso de Construcción -DLoad maneja automáticamente el proceso de construcción: +DLoad maneja automáticamente todo el proceso de construcción: 1. **Verificación de Golang**: Verifica que Go esté instalado globalmente (dependencia requerida) -2. **Preparación de Velox**: Usa Velox desde instalación global, descarga local, o descarga automáticamente si es necesario -3. **Configuración**: Copia tu velox.toml local al directorio de construcción +2. **Preparación de Velox**: Usa Velox desde instalación global, descarga local, o lo descarga automáticamente si es necesario +3. **Configuración**: Copia tu archivo velox.toml local al directorio de construcción 4. **Construcción**: Ejecuta el comando `vx build` con la configuración especificada -5. **Instalación**: Mueve el binario construido a la ubicación objetivo y establece permisos de ejecución +5. **Instalación**: Mueve el binario construido a la ubicación de destino y establece permisos de ejecución 6. **Limpieza**: Elimina archivos temporales de construcción > [!NOTE] @@ -369,13 +369,13 @@ DLoad maneja automáticamente el proceso de construcción: ### Generación de Archivo de Configuración -Puedes generar un archivo de configuración `velox.toml` usando el constructor en línea en https://build.roadrunner.dev/ +Puedes generar un archivo de configuración `velox.toml` usando el constructor online en https://build.roadrunner.dev/ Para documentación detallada sobre opciones de configuración de Velox y ejemplos, visita https://docs.roadrunner.dev/docs/customization/build -Esta interfaz web te ayuda a seleccionar plugins y genera la configuración apropiada para tu construcción RoadRunner personalizada. +Esta interfaz web te ayuda a seleccionar plugins y genera la configuración apropiada para tu build personalizado de RoadRunner. -### Usando Velox Descargado +### Usar Velox Descargado Puedes descargar Velox como parte de tu proceso de construcción en lugar de depender de una versión instalada globalmente: @@ -406,21 +406,21 @@ Esto asegura versiones consistentes de Velox entre diferentes entornos y miembro ``` -### Construyendo RoadRunner +### Construir RoadRunner ```bash -# Construir RoadRunner usando configuración velox.toml +# Construir RoadRunner usando la configuración velox.toml ./vendor/bin/dload build -# Construir con archivo de configuración específico +# Construir con un archivo de configuración específico ./vendor/bin/dload build --config=custom-rr.xml ``` -El binario RoadRunner construido incluirá solo los plugins especificados en tu archivo `velox.toml`, reduciendo el tamaño del binario y mejorando el rendimiento para tu caso de uso específico. +El binario de RoadRunner construido incluirá solo los plugins especificados en tu archivo `velox.toml`, reduciendo el tamaño del binario y mejorando el rendimiento para tu caso de uso específico. ## Registro de Software Personalizado -### Definiendo Software +### Definir Software ```xml @@ -433,15 +433,15 @@ El binario RoadRunner construido incluirá solo los plugins especificados en tu - - + + - + @@ -459,27 +459,27 @@ El binario RoadRunner construido incluirá solo los plugins especificados en tu ### Elementos de Software -#### Configuración de Repositorio +#### Configuración de Repository - **type**: Actualmente soporta "github" - **uri**: Ruta del repositorio (ej., "username/repo") -- **asset-pattern**: Patrón de expresión regular para coincidir con recursos de lanzamiento +- **asset-pattern**: Patrón regex para hacer match con assets de release -#### Elementos Binarios +#### Elementos Binary - **name**: Nombre del binario para referencia -- **pattern**: Patrón de expresión regular para coincidir con binario en recursos -- Maneja automáticamente filtrado por SO/arquitectura +- **pattern**: Patrón regex para hacer match con el binario en los assets +- Maneja automáticamente el filtrado por SO/arquitectura -#### Elementos de Archivo +#### Elementos File -- **pattern**: Patrón de expresión regular para coincidir con archivos +- **pattern**: Patrón regex para hacer match con archivos - **extract-path**: Directorio de extracción opcional - Funciona en cualquier sistema (sin filtrado por SO/arquitectura) ## Casos de Uso -### Configuración de Entorno de Desarrollo +### Configurar Entorno de Desarrollo ```bash # Configuración única para nuevos desarrolladores @@ -488,10 +488,10 @@ composer install ./vendor/bin/dload get ``` -### Configuración de Nuevo Proyecto +### Configurar Nuevo Proyecto ```bash -# Iniciar un nuevo proyecto con DLoad +# Empezar un nuevo proyecto con DLoad composer init composer require internal/dload -W ./vendor/bin/dload init @@ -512,7 +512,7 @@ Cada desarrollador obtiene los binarios correctos para su sistema: ```xml - + ``` @@ -526,11 +526,11 @@ Cada desarrollador obtiene los binarios correctos para su sistema: - + ``` -### Distribución de Recursos Frontend +### Distribución de Assets Frontend ```xml @@ -543,7 +543,7 @@ Cada desarrollador obtiene los binarios correctos para su sistema: ``` -## Límites de Rate de API de GitHub +## Límites de Rate de la API de GitHub Usa un token de acceso personal para evitar límites de rate: @@ -551,12 +551,12 @@ Usa un token de acceso personal para evitar límites de rate: GITHUB_TOKEN=your_token_here ./vendor/bin/dload get ``` -Añádelo a variables de entorno CI/CD para descargas automatizadas. +Agrégalo a las variables de entorno CI/CD para descargas automatizadas. -## Contribuciones +## Contribuir ¡Las contribuciones son bienvenidas! Envía Pull Requests para: -- Añadir nuevo software al registro predefinido +- Agregar nuevo software al registro predefinido - Mejorar la funcionalidad de DLoad - Mejorar la documentación y traducirla a [otros idiomas](docs/guidelines/how-to-translate-readme-docs.md) diff --git a/README-ru.md b/README-ru.md index 4ba4241..e3919f8 100644 --- a/README-ru.md +++ b/README-ru.md @@ -4,7 +4,7 @@ -

Легкая загрузка артефактов

+

Скачивай артефакты на раз

@@ -14,59 +14,60 @@
-DLoad упрощает загрузку и управление бинарными артефактами для ваших проектов. Идеально подходит для сред разработки, которые требуют специфических инструментов, таких как RoadRunner, Temporal или пользовательские бинарные файлы. +DLoad упрощает загрузку и управление бинарными артефактами в ваших проектах. Отлично подходит для dev-окружений, которым нужны специфические инструменты вроде RoadRunner, Temporal или собственные бинарники. [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%BA%F0%9F%87%B8-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![Russian readme](https://img.shields.io/badge/README-Русский%20%F0%9F%87%B7%F0%9F%87%BA-moccasin?style=flat-square)](README-ru.md) [![Spanish readme](https://img.shields.io/badge/README-Español%20%F0%9F%87%AA%F0%9F%87%B8-moccasin?style=flat-square)](README-es.md) -## Почему DLoad? +## Зачем нужен DLoad? + +DLoad решает распространённую проблему в PHP-проектах: как распространять и устанавливать нужные бинарные инструменты и ресурсы вместе с PHP-кодом. -DLoad решает общую проблему в PHP-проектах: как распространять и устанавливать необходимые бинарные инструменты и ресурсы вместе с PHP-кодом. С DLoad вы можете: -- Автоматически загружать необходимые инструменты во время инициализации проекта -- Обеспечить использование одинаковых версий инструментов всеми участниками команды -- Упростить адаптацию через автоматизацию настройки окружения -- Управлять кроссплатформенной совместимостью без ручной конфигурации -- Хранить бинарные файлы и ресурсы отдельно от системы контроля версий +- Автоматически скачивать необходимые инструменты при инициализации проекта +- Обеспечить использование одинаковых версий инструментов всей командой +- Упростить онбординг через автоматизацию настройки окружения +- Управлять кроссплатформенной совместимостью без ручной настройки +- Хранить бинарники и ресурсы отдельно от системы контроля версий ### Содержание - [Установка](#установка) - [Быстрый старт](#быстрый-старт) -- [Использование командной строки](#использование-командной-строки) +- [Использование в командной строке](#использование-в-командной-строке) - [Инициализация конфигурации](#инициализация-конфигурации) - [Загрузка ПО](#загрузка-по) - [Просмотр ПО](#просмотр-по) - - [Сборка пользовательского ПО](#сборка-пользовательского-по) + - [Сборка кастомного ПО](#сборка-кастомного-по) - [Руководство по конфигурации](#руководство-по-конфигурации) - [Интерактивная конфигурация](#интерактивная-конфигурация) - [Ручная конфигурация](#ручная-конфигурация) - [Типы загрузки](#типы-загрузки) - [Ограничения версий](#ограничения-версий) - - [Расширенные опции конфигурации](#расширенные-опции-конфигурации) -- [Сборка пользовательского RoadRunner](#сборка-пользовательского-roadrunner) - - [Конфигурация действия сборки](#конфигурация-действия-сборки) - - [Атрибуты действия Velox](#атрибуты-действия-velox) + - [Расширенные настройки](#расширенные-настройки) +- [Сборка кастомного RoadRunner](#сборка-кастомного-roadrunner) + - [Настройка действия сборки](#настройка-действия-сборки) + - [Атрибуты Velox-действия](#атрибуты-velox-действия) - [Процесс сборки](#процесс-сборки) - - [Генерация файла конфигурации](#генерация-файла-конфигурации) - - [Использование загруженного Velox](#использование-загруженного-velox) + - [Генерация конфигурационного файла](#генерация-конфигурационного-файла) + - [Использование скачанного Velox](#использование-скачанного-velox) - [Конфигурация DLoad](#конфигурация-dload) - [Сборка RoadRunner](#сборка-roadrunner) - [Пользовательский реестр ПО](#пользовательский-реестр-по) - [Определение ПО](#определение-по) - [Элементы ПО](#элементы-по) -- [Случаи использования](#случаи-использования) +- [Сценарии использования](#сценарии-использования) - [Настройка среды разработки](#настройка-среды-разработки) - [Настройка нового проекта](#настройка-нового-проекта) - - [Интеграция CI/CD](#интеграция-cicd) + - [Интеграция с CI/CD](#интеграция-с-cicd) - [Кроссплатформенные команды](#кроссплатформенные-команды) - - [Управление PHAR инструментами](#управление-phar-инструментами) - - [Распространение фронтенд ресурсов](#распространение-фронтенд-ресурсов) -- [Ограничения API GitHub](#ограничения-api-github) -- [Вклад в проект](#вклад-в-проект) + - [Управление PHAR-инструментами](#управление-phar-инструментами) + - [Распространение фронтенд-ресурсов](#распространение-фронтенд-ресурсов) +- [Ограничения GitHub API](#ограничения-github-api) +- [Участие в разработке](#участие-в-разработке) ## Установка @@ -88,15 +89,15 @@ composer require internal/dload -W composer require internal/dload -W ``` -Альтернативно, вы можете скачать последний релиз с [GitHub releases](https://github.com/php-internal/dload/releases). +Альтернативно можно скачать последний релиз с [GitHub releases](https://github.com/php-internal/dload/releases). -2. **Создайте файл конфигурации интерактивно**: +2. **Создайте конфигурационный файл интерактивно**: ```bash ./vendor/bin/dload init ``` - Эта команда проведет вас через выбор пакетов ПО и создаст файл конфигурации `dload.xml`. Вы также можете создать его вручную: + Эта команда проведёт вас через выбор пакетов ПО и создаст файл конфигурации `dload.xml`. Можно также создать его вручную: ```xml @@ -110,13 +111,13 @@ composer require internal/dload -W ``` -3. **Загрузите настроенное ПО**: +3. **Скачайте настроенное ПО**: ```bash ./vendor/bin/dload get ``` -4. **Интеграция с Composer** (опционально): +4. **Интегрируйте с Composer** (опционально): ```json { @@ -126,18 +127,18 @@ composer require internal/dload -W } ``` -## Использование командной строки +## Использование в командной строке ### Инициализация конфигурации ```bash -# Создать файл конфигурации интерактивно +# Создать конфигурационный файл интерактивно ./vendor/bin/dload init -# Создать конфигурацию в определенном месте +# Создать конфигурацию в определённом месте ./vendor/bin/dload init --config=./custom-dload.xml -# Создать минимальную конфигурацию без подсказок +# Создать минимальную конфигурацию без запросов ./vendor/bin/dload init --no-interaction # Перезаписать существующую конфигурацию без подтверждения @@ -147,82 +148,82 @@ composer require internal/dload -W ### Загрузка ПО ```bash -# Загрузить из файла конфигурации +# Загрузить из конфигурационного файла ./vendor/bin/dload get -# Загрузить определенные пакеты +# Загрузить конкретные пакеты ./vendor/bin/dload get rr temporal -# Загрузить с опциями +# Загрузить с дополнительными опциями ./vendor/bin/dload get rr --stability=beta --force ``` #### Опции загрузки | Опция | Описание | По умолчанию | -|--------|-------------|---------| -| `--path` | Директория для хранения бинарных файлов | Текущая директория | +|-------|----------|--------------| +| `--path` | Папка для хранения бинарников | Текущая папка | | `--arch` | Целевая архитектура (amd64, arm64) | Архитектура системы | | `--os` | Целевая ОС (linux, darwin, windows) | Текущая ОС | | `--stability` | Стабильность релиза (stable, beta) | stable | -| `--config` | Путь к файлу конфигурации | ./dload.xml | -| `--force`, `-f` | Принудительная загрузка даже если бинарный файл существует | false | +| `--config` | Путь к конфигурационному файлу | ./dload.xml | +| `--force`, `-f` | Принудительная загрузка даже если бинарник уже есть | false | ### Просмотр ПО ```bash -# Вывести список доступных пакетов ПО +# Показать доступные пакеты ПО ./vendor/bin/dload software # Показать загруженное ПО ./vendor/bin/dload show -# Показать детали определенного ПО +# Показать детали конкретного ПО ./vendor/bin/dload show rr -# Показать все ПО (загруженное и доступное) +# Показать всё ПО (загруженное и доступное) ./vendor/bin/dload show --all ``` -### Сборка пользовательского ПО +### Сборка кастомного ПО ```bash -# Собрать пользовательское ПО, используя файл конфигурации +# Собрать кастомное ПО используя конфигурационный файл ./vendor/bin/dload build -# Собрать с определенным файлом конфигурации +# Собрать с определённым конфигурационным файлом ./vendor/bin/dload build --config=./custom-dload.xml ``` #### Опции сборки | Опция | Описание | По умолчанию | -|--------|-------------|---------| -| `--config` | Путь к файлу конфигурации | ./dload.xml | +|-------|----------|--------------| +| `--config` | Путь к конфигурационному файлу | ./dload.xml | -Команда `build` выполняет действия сборки, определенные в вашем файле конфигурации, такие как создание пользовательских бинарных файлов RoadRunner с определенными плагинами. -Для подробной информации о сборке пользовательского RoadRunner смотрите раздел [Сборка пользовательского RoadRunner](#сборка-пользовательского-roadrunner). +Команда `build` выполняет действия сборки, определённые в вашем конфигурационном файле, например создание кастомных бинарников RoadRunner с определёнными плагинами. +Подробную информацию о сборке кастомного RoadRunner смотрите в разделе [Сборка кастомного RoadRunner](#сборка-кастомного-roadrunner). ## Руководство по конфигурации ### Интерактивная конфигурация -Самый простой способ создать файл конфигурации - использовать интерактивную команду `init`: +Простейший способ создать конфигурационный файл — использовать интерактивную команду `init`: ```bash ./vendor/bin/dload init ``` -Это: +Она: -- Проведет вас через выбор пакетов ПО +- Проведёт вас через выбор пакетов ПО - Покажет доступное ПО с описаниями и репозиториями -- Создаст правильно отформатированный файл `dload.xml` с валидацией схемы -- Корректно обработает существующие файлы конфигурации +- Сгенерирует правильно отформатированный файл `dload.xml` с валидацией схемы +- Аккуратно обработает существующие конфигурационные файлы ### Ручная конфигурация -Создайте `dload.xml` в корне вашего проекта: +Создайте `dload.xml` в корне проекта: ```xml @@ -239,51 +240,51 @@ composer require internal/dload -W ### Типы загрузки -DLoad поддерживает три типа загрузки, которые определяют, как обрабатываются ресурсы: +DLoad поддерживает три типа загрузки, которые определяют способ обработки ресурсов: #### Атрибут типа ```xml - - - + + + - + ``` #### Поведение по умолчанию (тип не указан) -Когда `type` не указан, DLoad автоматически использует все доступные обработчики: +Когда `type` не указан, DLoad автоматически применяет все доступные обработчики: -- **Обработка бинарных файлов**: Если у ПО есть секция ``, выполняет проверку наличия и версии бинарного файла +- **Обработка бинарников**: Если у ПО есть секция ``, выполняет проверку наличия и версии бинарника - **Обработка файлов**: Если у ПО есть секция `` и ресурс загружен, обрабатывает файлы во время распаковки - **Простая загрузка**: Если секций нет, загружает ресурс без распаковки ```xml - + - - + + ``` -#### Поведение явных типов +#### Поведение при явном указании типа -| Тип | Поведение | Случай использования | -|-----------|--------------------------------------------------------------|--------------------------------| -| `binary` | Проверка бинарных файлов, валидация версии, права на выполнение | CLI инструменты, исполняемые файлы | -| `phar` | Загружает `.phar` файлы как исполняемые **без распаковки** | PHP инструменты как Psalm, PHPStan | -| `archive` | **Принудительная распаковка даже для .phar файлов** | Когда нужно содержимое архива | +| Тип | Поведение | Случаи использования | +|-----------|----------------------------------------------------------------|--------------------------------| +| `binary` | Проверка бинарника, валидация версии, права на выполнение | CLI-инструменты, исполняемые файлы | +| `phar` | Загружает `.phar` файлы как исполняемые **без распаковки** | PHP-инструменты вроде Psalm, PHPStan | +| `archive` | **Принудительно распаковывает даже .phar файлы** | Когда нужно содержимое архива | > [!NOTE] -> Используйте `type="phar"` для PHP инструментов, которые должны остаться `.phar` файлами. +> Используйте `type="phar"` для PHP-инструментов, которые должны остаться как `.phar` файлы. > Использование `type="archive"` распакует даже `.phar` архивы. ### Ограничения версий @@ -295,46 +296,46 @@ DLoad поддерживает три типа загрузки, которые - + - + ``` -### Расширенные опции конфигурации +### Расширенные настройки ```xml - + - + ``` -## Сборка пользовательского RoadRunner +## Сборка кастомного RoadRunner -DLoad поддерживает сборку пользовательских бинарных файлов RoadRunner с использованием инструмента сборки Velox. Это полезно, когда вам нужен RoadRunner с пользовательскими комбинациями плагинов, которые недоступны в готовых релизах. +DLoad поддерживает сборку кастомных бинарников RoadRunner с помощью инструмента сборки Velox. Это полезно когда нужен RoadRunner с определёнными комбинациями плагинов, которые недоступны в готовых релизах. -### Конфигурация действия сборки +### Настройка действия сборки ```xml - + - + ``` -### Атрибуты действия Velox +### Атрибуты Velox-действия | Атрибут | Описание | По умолчанию | -|-----------|-------------|---------| +|---------|----------|--------------| | `velox-version` | Версия инструмента сборки Velox | Последняя | | `golang-version` | Требуемая версия Go | Последняя | | `binary-version` | Версия RoadRunner для отображения в `rr --version` | Последняя | | `config-file` | Путь к локальному файлу velox.toml | `./velox.toml` | -| `binary-path` | Путь для сохранения собранного бинарного файла RoadRunner | `./rr` | +| `binary-path` | Путь для сохранения собранного бинарника RoadRunner | `./rr` | ### Процесс сборки -DLoad автоматически обрабатывает процесс сборки: +DLoad автоматически управляет процессом сборки: -1. **Проверка Golang**: Проверяет, что Go установлен глобально (обязательная зависимость) -2. **Подготовка Velox**: Использует Velox из глобальной установки, локальной загрузки или автоматически загружает при необходимости -3. **Конфигурация**: Копирует ваш локальный velox.toml в директорию сборки +1. **Проверка Golang**: Проверяет что Go установлен глобально (обязательная зависимость) +2. **Подготовка Velox**: Использует Velox из глобальной установки, локальной загрузки или автоматически скачивает при необходимости +3. **Конфигурация**: Копирует ваш локальный velox.toml в папку сборки 4. **Сборка**: Выполняет команду `vx build` с указанной конфигурацией -5. **Установка**: Перемещает собранный бинарный файл в целевое место и устанавливает права на выполнение +5. **Установка**: Перемещает собранный бинарник в целевое расположение и устанавливает права на выполнение 6. **Очистка**: Удаляет временные файлы сборки > [!NOTE] -> DLoad требует, чтобы Go (Golang) был установлен глобально в вашей системе. Он не загружает и не управляет установками Go. +> DLoad требует чтобы Go (Golang) был установлен глобально в вашей системе. Он не скачивает и не управляет установками Go. -### Генерация файла конфигурации +### Генерация конфигурационного файла -Вы можете сгенерировать файл конфигурации `velox.toml`, используя онлайн-конструктор на https://build.roadrunner.dev/ +Можно сгенерировать файл конфигурации `velox.toml` с помощью онлайн-билдера на https://build.roadrunner.dev/ -Для подробной документации по опциям конфигурации Velox и примерам посетите https://docs.roadrunner.dev/docs/customization/build +Подробную документацию по опциям конфигурации Velox и примерам смотрите на https://docs.roadrunner.dev/docs/customization/build -Этот веб-интерфейс помогает выбрать плагины и создает соответствующую конфигурацию для вашей пользовательской сборки RoadRunner. +Этот веб-интерфейс помогает выбрать плагины и генерирует подходящую конфигурацию для вашей кастомной сборки RoadRunner. -### Использование загруженного Velox +### Использование скачанного Velox -Вы можете загрузить Velox как часть процесса сборки вместо использования глобально установленной версии: +Можно скачать Velox как часть процесса сборки вместо использования глобально установленной версии: ```xml @@ -388,7 +389,7 @@ DLoad автоматически обрабатывает процесс сбо ``` -Это обеспечивает согласованные версии Velox в разных окружениях и среди участников команды. +Это обеспечивает консистентные версии Velox в разных окружениях и между участниками команды. ### Конфигурация DLoad @@ -409,14 +410,14 @@ DLoad автоматически обрабатывает процесс сбо ### Сборка RoadRunner ```bash -# Собрать RoadRunner, используя конфигурацию velox.toml +# Собрать RoadRunner используя конфигурацию velox.toml ./vendor/bin/dload build -# Собрать с определенным файлом конфигурации +# Собрать с определённым конфигурационным файлом ./vendor/bin/dload build --config=custom-rr.xml ``` -Собранный бинарный файл RoadRunner будет включать только плагины, указанные в вашем файле `velox.toml`, уменьшая размер бинарного файла и улучшая производительность для вашего конкретного случая использования. +Собранный бинарник RoadRunner будет включать только плагины, указанные в вашем файле `velox.toml`, что уменьшает размер бинарника и улучшает производительность для вашего конкретного случая использования. ## Пользовательский реестр ПО @@ -425,7 +426,7 @@ DLoad автоматически обрабатывает процесс сбо ```xml - + @@ -434,13 +435,13 @@ DLoad автоматически обрабатывает процесс сбо - + - + @@ -448,7 +449,7 @@ DLoad автоматически обрабатывает процесс сбо - + @@ -463,21 +464,21 @@ DLoad автоматически обрабатывает процесс сбо - **type**: В настоящее время поддерживает "github" - **uri**: Путь репозитория (например, "username/repo") -- **asset-pattern**: Шаблон регулярного выражения для сопоставления ресурсов релиза +- **asset-pattern**: Regex-паттерн для соответствия ресурсам релиза -#### Элементы бинарных файлов +#### Элементы Binary -- **name**: Имя бинарного файла для ссылки -- **pattern**: Шаблон регулярного выражения для сопоставления бинарного файла в ресурсах +- **name**: Имя бинарника для ссылки +- **pattern**: Regex-паттерн для соответствия бинарнику в ресурсах - Автоматически обрабатывает фильтрацию по ОС/архитектуре -#### Элементы файлов +#### Элементы File -- **pattern**: Шаблон регулярного выражения для сопоставления файлов -- **extract-path**: Необязательная директория извлечения +- **pattern**: Regex-паттерн для соответствия файлам +- **extract-path**: Опциональная папка извлечения - Работает на любой системе (без фильтрации по ОС/архитектуре) -## Случаи использования +## Сценарии использования ### Настройка среды разработки @@ -498,7 +499,7 @@ composer require internal/dload -W ./vendor/bin/dload get ``` -### Интеграция CI/CD +### Интеграция с CI/CD ```yaml # GitHub Actions @@ -508,29 +509,29 @@ composer require internal/dload -W ### Кроссплатформенные команды -Каждый разработчик получает правильные бинарные файлы для своей системы: +Каждый разработчик получает правильные бинарники для своей системы: ```xml - - + + ``` -### Управление PHAR инструментами +### Управление PHAR-инструментами ```xml - + - + ``` -### Распространение фронтенд ресурсов +### Распространение фронтенд-ресурсов ```xml @@ -543,9 +544,9 @@ composer require internal/dload -W ``` -## Ограничения API GitHub +## Ограничения GitHub API -Используйте персональный токен доступа, чтобы избежать ограничений скорости: +Используйте персональный токен доступа чтобы избежать ограничений: ```bash GITHUB_TOKEN=your_token_here ./vendor/bin/dload get @@ -553,10 +554,10 @@ GITHUB_TOKEN=your_token_here ./vendor/bin/dload get Добавьте в переменные окружения CI/CD для автоматических загрузок. -## Вклад в проект +## Участие в разработке -Вклады приветствуются! Отправляйте Pull Request для: +Участие приветствуется! Отправляйте Pull Request'ы для: -- Добавления нового ПО в предопределенный реестр -- Улучшения функциональности DLoad -- Улучшения документации и ее перевода на [другие языки](docs/guidelines/how-to-translate-readme-docs.md) +- Добавления нового ПО в предопределённый реестр +- Улучшения функциональности DLoad +- Улучшения документации и перевода на [другие языки](docs/guidelines/how-to-translate-readme-docs.md) diff --git a/README-zh.md b/README-zh.md index 719cb11..60a54e4 100644 --- a/README-zh.md +++ b/README-zh.md @@ -14,7 +14,7 @@
-DLoad 简化了为您的项目下载和管理二进制工件的过程。非常适合需要特定工具(如 RoadRunner、Temporal 或自定义二进制文件)的开发环境。 +DLoad 让项目中的二进制工件下载和管理变得简单轻松。它特别适合那些需要特定工具的开发环境,比如 RoadRunner、Temporal 或者自定义二进制文件。 [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%BA%F0%9F%87%B8-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) @@ -23,19 +23,19 @@ DLoad 简化了为您的项目下载和管理二进制工件的过程。非常 ## 为什么选择 DLoad? -DLoad 解决了 PHP 项目中的一个常见问题:如何在 PHP 代码的同时分发和安装必要的二进制工具和资产。 -使用 DLoad,您可以: +DLoad 解决了 PHP 项目中的一个实际问题:如何在分发 PHP 代码的同时,有效地分发和安装必要的二进制工具和资产。 +通过 DLoad,你可以: -- 在项目初始化期间自动下载所需工具 -- 确保所有团队成员使用相同版本的工具 -- 通过自动化环境设置简化新人入职 -- 管理跨平台兼容性,无需手动配置 -- 将二进制文件和资产与版本控制分开 +- 在项目初始化时自动下载所需工具 +- 确保团队所有成员都用相同版本的工具 +- 通过自动化环境配置简化新人入职流程 +- 无需手动配置就能管理跨平台兼容性 +- 让二进制文件和资产与版本控制保持分离 ### 目录 - [安装](#安装) -- [快速开始](#快速开始) +- [快速上手](#快速上手) - [命令行使用](#命令行使用) - [初始化配置](#初始化配置) - [下载软件](#下载软件) @@ -48,25 +48,25 @@ DLoad 解决了 PHP 项目中的一个常见问题:如何在 PHP 代码的同 - [版本约束](#版本约束) - [高级配置选项](#高级配置选项) - [构建自定义 RoadRunner](#构建自定义-roadrunner) - - [构建操作配置](#构建操作配置) - - [Velox 操作属性](#velox-操作属性) - - [构建过程](#构建过程) + - [构建动作配置](#构建动作配置) + - [Velox 动作属性](#velox-动作属性) + - [构建流程](#构建流程) - [配置文件生成](#配置文件生成) - [使用下载的 Velox](#使用下载的-velox) - [DLoad 配置](#dload-配置) - [构建 RoadRunner](#构建-roadrunner) - [自定义软件注册表](#自定义软件注册表) - [定义软件](#定义软件) - - [软件元素](#软件元素) -- [用例](#用例) - - [开发环境设置](#开发环境设置) - - [新项目设置](#新项目设置) + - [软件要素](#软件要素) +- [使用场景](#使用场景) + - [开发环境配置](#开发环境配置) + - [新项目创建](#新项目创建) - [CI/CD 集成](#cicd-集成) - - [跨平台团队](#跨平台团队) + - [跨平台团队协作](#跨平台团队协作) - [PHAR 工具管理](#phar-工具管理) - - [前端资产分发](#前端资产分发) + - [前端资源分发](#前端资源分发) - [GitHub API 速率限制](#github-api-速率限制) -- [贡献](#贡献) +- [参与贡献](#参与贡献) ## 安装 @@ -80,7 +80,7 @@ composer require internal/dload -W [![License](https://img.shields.io/packagist/l/internal/dload.svg?style=flat-square)](LICENSE.md) [![Total DLoads](https://img.shields.io/packagist/dt/internal/dload.svg?style=flat-square)](https://packagist.org/packages/internal/dload/stats) -## 快速开始 +## 快速上手 1. **通过 Composer 安装 DLoad**: @@ -88,7 +88,7 @@ composer require internal/dload -W composer require internal/dload -W ``` -或者,您可以从 [GitHub 发布页面](https://github.com/php-internal/dload/releases) 下载最新版本。 + 你也可以从 [GitHub 发布页面](https://github.com/php-internal/dload/releases) 下载最新版本。 2. **交互式创建配置文件**: @@ -96,7 +96,7 @@ composer require internal/dload -W ./vendor/bin/dload init ``` - 此命令将指导您选择软件包并创建 `dload.xml` 配置文件。您也可以手动创建: + 这个命令会引导你选择软件包,并创建一个 `dload.xml` 配置文件。当然,你也可以手动创建: ```xml @@ -110,7 +110,7 @@ composer require internal/dload -W ``` -3. **下载配置的软件**: +3. **下载配置好的软件**: ```bash ./vendor/bin/dload get @@ -137,41 +137,41 @@ composer require internal/dload -W # 在指定位置创建配置 ./vendor/bin/dload init --config=./custom-dload.xml -# 创建最小配置,无提示 +# 无提示创建最简配置 ./vendor/bin/dload init --no-interaction -# 覆盖现有配置而不确认 +# 不经确认就覆盖现有配置 ./vendor/bin/dload init --overwrite ``` ### 下载软件 ```bash -# 从配置文件下载 +# 根据配置文件下载 ./vendor/bin/dload get -# 下载特定包 +# 下载指定软件包 ./vendor/bin/dload get rr temporal -# 使用选项下载 +# 带选项下载 ./vendor/bin/dload get rr --stability=beta --force ``` #### 下载选项 -| 选项 | 描述 | 默认值 | +| 选项 | 说明 | 默认值 | |--------|-------------|---------| -| `--path` | 存储二进制文件的目录 | 当前目录 | +| `--path` | 二进制文件存储目录 | 当前目录 | | `--arch` | 目标架构 (amd64, arm64) | 系统架构 | | `--os` | 目标操作系统 (linux, darwin, windows) | 当前操作系统 | | `--stability` | 发布稳定性 (stable, beta) | stable | | `--config` | 配置文件路径 | ./dload.xml | -| `--force`, `-f` | 即使二进制文件存在也强制下载 | false | +| `--force`, `-f` | 即使二进制文件已存在也强制下载 | false | ### 查看软件 ```bash -# 列出可用的软件包 +# 列出可用软件包 ./vendor/bin/dload software # 显示已下载的软件 @@ -196,28 +196,28 @@ composer require internal/dload -W #### 构建选项 -| 选项 | 描述 | 默认值 | +| 选项 | 说明 | 默认值 | |--------|-------------|---------| | `--config` | 配置文件路径 | ./dload.xml | -`build` 命令执行配置文件中定义的构建操作,例如创建具有特定插件的自定义 RoadRunner 二进制文件。 -有关构建自定义 RoadRunner 的详细信息,请参阅 [构建自定义 RoadRunner](#构建自定义-roadrunner) 部分。 +`build` 命令会执行配置文件中定义的构建动作,比如创建带有特定插件的自定义 RoadRunner 二进制文件。 +想了解构建自定义 RoadRunner 的详细信息,请查看 [构建自定义 RoadRunner](#构建自定义-roadrunner) 部分。 ## 配置指南 ### 交互式配置 -创建配置文件的最简单方法是使用交互式 `init` 命令: +创建配置文件最简单的方式就是使用交互式 `init` 命令: ```bash ./vendor/bin/dload init ``` -这将: +这个命令会: -- 指导您选择软件包 -- 显示可用软件及其描述和仓库 -- 生成格式正确的 `dload.xml` 文件并进行模式验证 +- 引导你选择软件包 +- 显示可用软件及其说明和仓库信息 +- 生成格式正确并带有模式验证的 `dload.xml` 文件 - 优雅地处理现有配置文件 ### 手动配置 @@ -239,28 +239,28 @@ composer require internal/dload -W ### 下载类型 -DLoad 支持三种下载类型,决定资产的处理方式: +DLoad 支持三种下载类型,它们决定了资源的处理方式: #### 类型属性 ```xml - + - - + + - + ``` #### 默认行为(未指定类型) -当未指定 `type` 时,DLoad 自动使用所有可用的处理器: +当没有指定 `type` 时,DLoad 会自动使用所有可用的处理器: -- **二进制处理**:如果软件有 `` 部分,执行二进制存在性和版本检查 -- **文件处理**:如果软件有 `` 部分且资产已下载,在解包期间处理文件 -- **简单下载**:如果没有部分存在,下载资产而不解包 +- **二进制处理**:如果软件有 `` 部分,会进行二进制存在性和版本检查 +- **文件处理**:如果软件有 `` 部分且资源已下载,会在解包时处理文件 +- **简单下载**:如果没有任何部分,则下载资源但不解包 ```xml @@ -269,26 +269,26 @@ DLoad 支持三种下载类型,决定资产的处理方式: - - + + ``` -#### 显式类型行为 +#### 明确类型的行为 -| 类型 | 行为 | 用例 | +| 类型 | 行为 | 适用场景 | |-----------|--------------------------------------------------------------|--------------------------------| | `binary` | 二进制检查、版本验证、可执行权限 | CLI 工具、可执行文件 | -| `phar` | 下载 `.phar` 文件作为可执行文件**而不解包** | PHP 工具如 Psalm、PHPStan | -| `archive` | **强制解包即使是 .phar 文件** | 当您需要归档内容时 | +| `phar` | 下载 `.phar` 文件作为可执行文件**但不解包** | PHP 工具如 Psalm、PHPStan | +| `archive` | **强制解包即使是 .phar 文件** | 当你需要压缩包内容时 | > [!NOTE] -> 对于应保持为 `.phar` 文件的 PHP 工具,使用 `type="phar"`。 -> 使用 `type="archive"` 将解包甚至 `.phar` 归档。 +> 对于应该保持为 `.phar` 文件的 PHP 工具,使用 `type="phar"`。 +> 使用 `type="archive"` 会解包甚至 `.phar` 压缩包。 ### 版本约束 -使用 Composer 风格的版本约束: +使用类似 Composer 的版本约束: ```xml @@ -302,7 +302,7 @@ DLoad 支持三种下载类型,决定资产的处理方式: - + ``` @@ -312,7 +312,7 @@ DLoad 支持三种下载类型,决定资产的处理方式: ```xml - + @@ -325,16 +325,16 @@ DLoad 支持三种下载类型,决定资产的处理方式: ## 构建自定义 RoadRunner -DLoad 支持使用 Velox 构建工具构建自定义 RoadRunner 二进制文件。当您需要具有预构建版本中不可用的自定义插件组合的 RoadRunner 时,这非常有用。 +DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文件。当你需要包含特定插件组合的 RoadRunner,而这些组合在预构建版本中不可用时,这功能就很有用了。 -### 构建操作配置 +### 构建动作配置 ```xml - + - + ``` -### Velox 操作属性 +### Velox 动作属性 -| 属性 | 描述 | 默认值 | +| 属性 | 说明 | 默认值 | |-----------|-------------|---------| -| `velox-version` | Velox 构建工具版本 | 最新 | -| `golang-version` | 所需的 Go 版本 | 最新 | -| `binary-version` | 在 `rr --version` 中显示的 RoadRunner 版本 | 最新 | +| `velox-version` | Velox 构建工具版本 | 最新版 | +| `golang-version` | 所需的 Go 版本 | 最新版 | +| `binary-version` | 在 `rr --version` 中显示的 RoadRunner 版本 | 最新版 | | `config-file` | 本地 velox.toml 文件路径 | `./velox.toml` | | `binary-path` | 保存构建的 RoadRunner 二进制文件的路径 | `./rr` | -### 构建过程 +### 构建流程 -DLoad 自动处理构建过程: +DLoad 会自动处理构建过程: -1. **Golang 检查**:验证 Go 是否全局安装(必需依赖项) -2. **Velox 准备**:使用全局安装的 Velox、本地下载或在需要时自动下载 -3. **配置**:将您的本地 velox.toml 复制到构建目录 +1. **Golang 检查**:验证 Go 是否全局安装(必需依赖) +2. **Velox 准备**:使用全局安装的 Velox、本地下载版本,或者在需要时自动下载 +3. **配置**:将你的本地 velox.toml 复制到构建目录 4. **构建**:使用指定配置执行 `vx build` 命令 -5. **安装**:将构建的二进制文件移动到目标位置并设置可执行权限 +5. **安装**:将构建好的二进制文件移动到目标位置并设置可执行权限 6. **清理**:删除临时构建文件 > [!NOTE] -> DLoad 需要在您的系统上全局安装 Go (Golang)。它不会下载或管理 Go 安装。 +> DLoad 需要在你的系统上全局安装 Go (Golang)。它不会下载或管理 Go 的安装。 ### 配置文件生成 -您可以使用 https://build.roadrunner.dev/ 上的在线构建器生成 `velox.toml` 配置文件 +你可以使用 https://build.roadrunner.dev/ 上的在线构建器来生成 `velox.toml` 配置文件。 -有关 Velox 配置选项和示例的详细文档,请访问 https://docs.roadrunner.dev/docs/customization/build +关于 Velox 配置选项和示例的详细文档,请访问 https://docs.roadrunner.dev/docs/customization/build -此 Web 界面帮助您选择插件并为您的自定义 RoadRunner 构建生成适当的配置。 +这个网页界面帮助你选择插件,并为你的自定义 RoadRunner 构建生成合适的配置。 ### 使用下载的 Velox -您可以将 Velox 作为构建过程的一部分下载,而不是依赖全局安装的版本: +你可以将 Velox 作为构建过程的一部分下载,而不是依赖全局安装的版本: ```xml @@ -388,7 +388,7 @@ DLoad 自动处理构建过程: ``` -这确保在不同环境和团队成员之间使用一致的 Velox 版本。 +这样可以确保在不同环境和团队成员之间使用一致的 Velox 版本。 ### DLoad 配置 @@ -416,7 +416,7 @@ DLoad 自动处理构建过程: ./vendor/bin/dload build --config=custom-rr.xml ``` -构建的 RoadRunner 二进制文件将仅包含您在 `velox.toml` 文件中指定的插件,从而减少二进制文件大小并提高特定用例的性能。 +构建好的 RoadRunner 二进制文件只会包含你在 `velox.toml` 文件中指定的插件,这样能减少二进制文件大小并提升特定用例的性能。 ## 自定义软件注册表 @@ -433,15 +433,15 @@ DLoad 自动处理构建过程: - - + + - - + + @@ -457,41 +457,41 @@ DLoad 自动处理构建过程: ``` -### 软件元素 +### 软件要素 #### 仓库配置 - **type**:目前支持 "github" -- **uri**:仓库路径(例如,"username/repo") -- **asset-pattern**:匹配发布资产的正则表达式模式 +- **uri**:仓库路径(例如 "username/repo") +- **asset-pattern**:匹配发布资源的正则表达式模式 -#### 二进制元素 +#### 二进制要素 -- **name**:用于引用的二进制名称 -- **pattern**:匹配资产中二进制文件的正则表达式模式 +- **name**:用于引用的二进制文件名 +- **pattern**:匹配资源中二进制文件的正则表达式模式 - 自动处理操作系统/架构过滤 -#### 文件元素 +#### 文件要素 - **pattern**:匹配文件的正则表达式模式 -- **extract-path**:可选的提取目录 -- 在任何系统上工作(无操作系统/架构过滤) +- **extract-path**:可选的解压目录 +- 在任何系统上都能工作(无操作系统/架构过滤) -## 用例 +## 使用场景 -### 开发环境设置 +### 开发环境配置 ```bash -# 新开发者的一次性设置 +# 新开发者的一次性配置 composer install -./vendor/bin/dload init # 仅第一次 +./vendor/bin/dload init # 仅第一次需要 ./vendor/bin/dload get ``` -### 新项目设置 +### 新项目创建 ```bash -# 使用 DLoad 启动新项目 +# 使用 DLoad 开始新项目 composer init composer require internal/dload -W ./vendor/bin/dload init @@ -506,14 +506,14 @@ composer require internal/dload -W run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} ./vendor/bin/dload get ``` -### 跨平台团队 +### 跨平台团队协作 -每个开发者获得适合其系统的正确二进制文件: +每个开发者都能获得适合其系统的正确二进制文件: ```xml - - + + ``` @@ -525,12 +525,12 @@ composer require internal/dload -W - + ``` -### 前端资产分发 +### 前端资源分发 ```xml @@ -545,17 +545,17 @@ composer require internal/dload -W ## GitHub API 速率限制 -使用个人访问令牌以避免速率限制: +使用个人访问令牌来避免速率限制: ```bash GITHUB_TOKEN=your_token_here ./vendor/bin/dload get ``` -将其添加到 CI/CD 环境变量中以进行自动下载。 +在 CI/CD 环境变量中添加此配置,以便自动下载。 -## 贡献 +## 参与贡献 -欢迎贡献!提交拉取请求以: +欢迎贡献!你可以提交拉取请求来: - 向预定义注册表添加新软件 - 改进 DLoad 功能 diff --git a/docs/guidelines/how-to-translate-readme-docs.md b/docs/guidelines/how-to-translate-readme-docs.md index 5d4de57..d92874a 100644 --- a/docs/guidelines/how-to-translate-readme-docs.md +++ b/docs/guidelines/how-to-translate-readme-docs.md @@ -28,6 +28,7 @@ I need a single message with all the md file content. - Command examples and their arguments: Verify technical accuracy and formatting. - Technical terminology consistency throughout the document. - Cultural adaptations that make sense in target language context. + - **Language naturalness**: Ensure the translation uses natural, living language that doesn't sound synthetic or machine-generated, while maintaining technical precision. Find the golden mean between conversational flow and technical accuracy. **Step 5: Finalize and Save the Translated Document 💾** @@ -91,6 +92,7 @@ CONTRIBUTING-de.md - Use clear and specific prompts to minimize errors. - Always double-check technical content, as LLMs may mistranslate code or markup. +- **Prioritize natural language flow**: Avoid overly literal translations that sound robotic. The text should read as if written by a native speaker while preserving technical accuracy. - Consider using frameworks like MAPS (Multi-Aspect Prompting and Selection) for complex translations, which guide the LLM through keywords, topics, and relevant examples to improve accuracy and reduce errors. - Remember: Human review is essential for catching subtle mistakes and ensuring the translation meets your quality standards. From 5a5a4b1074030aba6d6e39624180e2cbe8353e2e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 12:33:33 +0400 Subject: [PATCH 26/38] fix(DLoad): initialize variables before use in binary extraction loop --- src/DLoad.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DLoad.php b/src/DLoad.php index 06e9911..2fa4229 100644 --- a/src/DLoad.php +++ b/src/DLoad.php @@ -37,9 +37,9 @@ * based on configuration actions. * * ```php - * $dload = $container->get(DLoad::class); - * $dload->addTask(new DownloadConfig('rr', '^2.12.0')); - * $dload->run(); + * $dload = $container->get(DLoad::class); + * $dload->addTask(new DownloadConfig('rr', '^2.12.0')); + * $dload->run(); * ``` * * @internal @@ -236,6 +236,7 @@ private function prepareExtractTask( $binaryPattern = $this->generateBinaryExtractionConfig($software->binary); while ($extractor->valid()) { + $to = $rule = null; $file = $extractor->current(); \assert($file instanceof \SplFileInfo); From 59b5c6bb19747f5130d28f274ee00cb076f07287 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 14:00:28 +0400 Subject: [PATCH 27/38] feat(velox): implement configuration strategies for local, remote, and hybrid setups, add ConfigBuilder --- src/Module/Velox/Internal/Config/Strategy.php | 29 +++ .../Velox/Internal/Config/Strategy/Hybrid.php | 52 +++++ .../Velox/Internal/Config/Strategy/Local.php | 57 +++++ .../Velox/Internal/Config/Strategy/Remote.php | 42 ++++ .../Velox/Internal/Config/TomlMerger.php | 199 ++++++++++++++++++ .../Velox/Internal/Config/Validator.php | 115 ++++++++++ src/Module/Velox/Internal/ConfigBuilder.php | 100 +++++++++ src/Module/Velox/Internal/VeloxBuilder.php | 33 +-- 8 files changed, 600 insertions(+), 27 deletions(-) create mode 100644 src/Module/Velox/Internal/Config/Strategy.php create mode 100644 src/Module/Velox/Internal/Config/Strategy/Hybrid.php create mode 100644 src/Module/Velox/Internal/Config/Strategy/Local.php create mode 100644 src/Module/Velox/Internal/Config/Strategy/Remote.php create mode 100644 src/Module/Velox/Internal/Config/TomlMerger.php create mode 100644 src/Module/Velox/Internal/Config/Validator.php create mode 100644 src/Module/Velox/Internal/ConfigBuilder.php diff --git a/src/Module/Velox/Internal/Config/Strategy.php b/src/Module/Velox/Internal/Config/Strategy.php new file mode 100644 index 0000000..fe0a9d5 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Strategy.php @@ -0,0 +1,29 @@ +localStrategy = new Local(); + $this->remoteStrategy = new Remote($apiClient); + } + + public function supports(VeloxAction $action): bool + { + return $action->configFile !== null && $action->plugins !== []; + } + + public function build(VeloxAction $action): string + { + $this->supports($action) or throw new ConfigException( + 'Hybrid config strategy requires both config file and plugins', + ); + + // Get local config content + $localConfig = $this->localStrategy->build($action); + + // Generate remote config from plugins + $remoteConfig = $this->remoteStrategy->build($action); + + // Merge configurations (remote plugins extend/override local) + return $this->tomlMerger->merge($localConfig, $remoteConfig); + } +} diff --git a/src/Module/Velox/Internal/Config/Strategy/Local.php b/src/Module/Velox/Internal/Config/Strategy/Local.php new file mode 100644 index 0000000..e591bd5 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Strategy/Local.php @@ -0,0 +1,57 @@ +configFile !== null && $action->plugins === []; + } + + public function build(VeloxAction $action): string + { + $configFile = $action->configFile ?? throw new ConfigException( + 'Local config strategy requires a config file', + ); + + $configPath = Path::create($configFile); + + $configPath->exists() or throw new ConfigException( + "Config file not found: {$configFile}", + configPath: $configFile, + ); + + $configPath->isFile() or throw new ConfigException( + "Config path is not a file: {$configFile}", + configPath: $configFile, + ); + + $configPath->isReadable() or throw new ConfigException( + "Config file is not readable: {$configFile}", + configPath: $configFile, + ); + + $content = \file_get_contents($configPath->__toString()); + + $content !== false or throw new ConfigException( + "Failed to read config file: {$configFile}", + configPath: $configFile, + ); + + return $content; + } +} diff --git a/src/Module/Velox/Internal/Config/Strategy/Remote.php b/src/Module/Velox/Internal/Config/Strategy/Remote.php new file mode 100644 index 0000000..3b2070d --- /dev/null +++ b/src/Module/Velox/Internal/Config/Strategy/Remote.php @@ -0,0 +1,42 @@ +configFile === null && $action->plugins !== []; + } + + public function build(VeloxAction $action): string + { + $action->plugins !== [] or throw new ConfigException( + 'Remote config strategy requires at least one plugin', + ); + + return $this->apiClient->generateConfig( + plugins: $action->plugins, + golangVersion: $action->golangVersion, + binaryVersion: $action->binaryVersion, + options: $action->options ?? [], + ); + } +} diff --git a/src/Module/Velox/Internal/Config/TomlMerger.php b/src/Module/Velox/Internal/Config/TomlMerger.php new file mode 100644 index 0000000..c07df07 --- /dev/null +++ b/src/Module/Velox/Internal/Config/TomlMerger.php @@ -0,0 +1,199 @@ +parseToml($localToml); + $remoteData = $this->parseToml($remoteToml); + + // Merge github.plugins sections + if (isset($remoteData['github']['plugins'])) { + $localData['github']['plugins'] = \array_merge( + $localData['github']['plugins'] ?? [], + $remoteData['github']['plugins'], + ); + } + + // Preserve roadrunner version from local unless remote specifies one + if (isset($remoteData['roadrunner']['ref']) && !isset($localData['roadrunner']['ref'])) { + $localData['roadrunner']['ref'] = $remoteData['roadrunner']['ref']; + } + + return $this->arrayToToml($localData); + } catch (\Throwable $e) { + throw new ConfigException( + 'Failed to merge TOML configurations: ' . $e->getMessage(), + previous: $e, + ); + } + } + + /** + * Simple TOML parser for basic configuration merging. + * + * @param string $toml TOML content + * @return array Parsed configuration + */ + private function parseToml(string $toml): array + { + $result = []; + $currentSection = null; + $lines = \explode("\n", $toml); + + foreach ($lines as $line) { + $line = \trim($line); + + // Skip empty lines and comments + if ($line === '' || \str_starts_with($line, '#')) { + continue; + } + + // Section headers like [roadrunner] or [github.plugins.logger] + if (\preg_match('/^\[([^\]]+)\]$/', $line, $matches)) { + $currentSection = $matches[1]; + continue; + } + + // Key-value pairs + if (\preg_match('/^([^=]+)=(.+)$/', $line, $matches)) { + $key = \trim($matches[1]); + $value = \trim($matches[2], ' "\''); + + if ($currentSection === null) { + $result[$key] = $value; + } else { + $this->setNestedValue($result, $currentSection . '.' . $key, $value); + } + } + } + + return $result; + } + + /** + * Sets a nested array value using dot notation. + * + * @param array $array Target array + * @param string $path Dot-separated path + * @param mixed $value Value to set + */ + private function setNestedValue(array &$array, string $path, mixed $value): void + { + $keys = \explode('.', $path); + $current = &$array; + + foreach ($keys as $key) { + if (!\is_array($current)) { + $current = []; + } + if (!isset($current[$key])) { + $current[$key] = []; + } + $current = &$current[$key]; + } + + $current = $value; + } + + /** + * Converts array back to TOML format. + * + * @param array $data Configuration data + * @return string TOML content + */ + private function arrayToToml(array $data): string + { + $toml = ''; + + // First output top-level keys + foreach ($data as $key => $value) { + if (!\is_array($value)) { + $toml .= "{$key} = \"{$value}\"\n"; + } + } + + if ($toml !== '') { + $toml .= "\n"; + } + + // Then output sections + foreach ($data as $sectionKey => $sectionValue) { + if (\is_array($sectionValue)) { + $toml .= $this->sectionToToml($sectionKey, $sectionValue); + } + } + + return $toml; + } + + /** + * Converts a section to TOML format. + * + * @param string $sectionName Section name + * @param array $data Section data + * @return string TOML section content + */ + private function sectionToToml(string $sectionName, array $data): string + { + $toml = ''; + + // Check if this section has subsections + $hasSubsections = false; + foreach ($data as $value) { + if (\is_array($value)) { + $hasSubsections = true; + break; + } + } + + if (!$hasSubsections) { + // Simple section with key-value pairs + $toml .= "[{$sectionName}]\n"; + foreach ($data as $key => $value) { + $toml .= "{$key} = \"{$value}\"\n"; + } + $toml .= "\n"; + } else { + // Section with subsections (like github.plugins) + foreach ($data as $subKey => $subValue) { + if (\is_array($subValue)) { + $toml .= "[{$sectionName}.{$subKey}]\n"; + foreach ($subValue as $key => $value) { + $toml .= "{$key} = \"{$value}\"\n"; + } + $toml .= "\n"; + } else { + $toml .= "[{$sectionName}]\n"; + $toml .= "{$subKey} = \"{$subValue}\"\n"; + $toml .= "\n"; + } + } + } + + return $toml; + } +} diff --git a/src/Module/Velox/Internal/Config/Validator.php b/src/Module/Velox/Internal/Config/Validator.php new file mode 100644 index 0000000..882c45d --- /dev/null +++ b/src/Module/Velox/Internal/Config/Validator.php @@ -0,0 +1,115 @@ +exists() || !$configPath->isFile()) { + return false; + } + + $content = \file_get_contents($configPath->__toString()); + if ($content === false) { + return false; + } + + // Basic TOML validation - check for required sections + return $this->hasValidTomlStructure($content); + } + + private static function validateConfigSource(VeloxAction $config): void + { + if ($config->configFile === null && $config->plugins === []) { + throw new ConfigException( + 'Velox configuration must specify either config-file or plugins list', + ); + } + } + + private static function validateLocalConfigFile(VeloxAction $config): void + { + if ($config->configFile === null) { + return; + } + + $configPath = Path::create($config->configFile); + + if (!$configPath->exists()) { + throw new ConfigException( + "Velox config file not found: {$config->configFile}", + configPath: $config->configFile, + ); + } + + if (!$configPath->isFile()) { + throw new ConfigException( + "Velox config path is not a file: {$config->configFile}", + configPath: $config->configFile, + ); + } + } + + /** + * Validates basic TOML structure for Velox configurations. + */ + private function hasValidTomlStructure(string $content): bool + { + // Check for basic TOML syntax errors + $lines = \explode("\n", $content); + + foreach ($lines as $line) { + $line = \trim($line); + + // Skip empty lines and comments + if ($line === '' || \str_starts_with($line, '#')) { + continue; + } + + // Check section headers + if (\str_starts_with($line, '[') && \str_ends_with($line, ']')) { + continue; + } + + // Check key-value pairs + if (\str_contains($line, '=')) { + continue; + } + + // Invalid line found + return false; + } + + return true; + } +} diff --git a/src/Module/Velox/Internal/ConfigBuilder.php b/src/Module/Velox/Internal/ConfigBuilder.php new file mode 100644 index 0000000..466e920 --- /dev/null +++ b/src/Module/Velox/Internal/ConfigBuilder.php @@ -0,0 +1,100 @@ +logger->debug('Building Velox configuration...'); + + $strategy = $this->getConfigStrategy($action); + $configContent = $strategy->build($action); + + $configPath = $buildDir->join('velox.toml'); + + \file_put_contents($configPath->__toString(), $configContent) or throw new ConfigException( + "Failed to write config file to: {$configPath}", + ); + + $this->logger->debug('Configuration written to: %s', (string) $configPath); + + // Validate the generated configuration + $this->validateConfig($configPath) or throw new ConfigException( + "Generated configuration is invalid: {$configPath}", + ); + + return $configPath; + } + + /** + * Validates that a configuration file is valid. + * + * @param Path $configPath Path to the configuration file to validate + * @return bool True if configuration is valid, false otherwise + */ + public function validateConfig(Path $configPath): bool + { + return $this->validator->validateTomlFile($configPath); + } + + /** + * Selects the appropriate configuration strategy based on action inputs. + */ + private function getConfigStrategy(VeloxAction $action): Strategy + { + return match (true) { + $action->configFile !== null && $action->plugins !== [] => new Hybrid($this->apiClient), + $action->configFile !== null => new Local(), + $action->plugins !== [] => new Remote($this->apiClient), + default => throw new ConfigException('No valid configuration source provided'), + }; + } +} diff --git a/src/Module/Velox/Internal/VeloxBuilder.php b/src/Module/Velox/Internal/VeloxBuilder.php index f263be8..544ebe2 100644 --- a/src/Module/Velox/Internal/VeloxBuilder.php +++ b/src/Module/Velox/Internal/VeloxBuilder.php @@ -14,7 +14,7 @@ use Internal\DLoad\Module\Config\Schema\Embed\Binary as BinaryConfig; use Internal\DLoad\Module\Velox\Builder; use Internal\DLoad\Module\Velox\Exception\Build as BuildException; -use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; +use Internal\DLoad\Module\Velox\Internal\Config\Validator; use Internal\DLoad\Module\Velox\Result; use Internal\DLoad\Module\Velox\Task; use Internal\DLoad\Service\Logger; @@ -24,10 +24,10 @@ use function React\Promise\resolve; /** - * Basic Velox builder implementation with local config support. + * Velox builder implementation with comprehensive config support. * * Provides a synchronous implementation for building RoadRunner binaries - * using Velox with local configuration files. + * using Velox with support for local configs, remote API configs, and hybrid merging. * * @internal * @psalm-internal Internal\DLoad\Module\Velox @@ -40,6 +40,7 @@ public function __construct( private readonly Downloader $appConfig, private readonly OperatingSystem $operatingSystem, private readonly BinaryProvider $binaryProvider, + private readonly ConfigBuilder $configBuilder, ) {} public function build(VeloxAction $config, \Closure $onProgress): Task @@ -68,7 +69,7 @@ public function build(VeloxAction $config, \Closure $onProgress): Task $vxBinary = $dependencyChecker->prepareVelox(); # Prepare configuration file - $configPath = $this->prepareConfig($config, $buildDir); + $configPath = $this->configBuilder->buildConfig($config, $buildDir); # Build # Execute build command @@ -97,29 +98,7 @@ public function build(VeloxAction $config, \Closure $onProgress): Task public function validate(VeloxAction $config): void { - ConfigValidator::validate($config); - - // For this basic implementation, only local config files are supported - if ($config->configFile === null) { - throw new ConfigException( - 'This implementation only supports local config files. Remote API configuration is not yet implemented.', - ); - } - } - - private function prepareConfig(VeloxAction $config, Path $buildDir): Path - { - $sourceConfig = Path::create($config->configFile ?? 'velox.toml'); - $targetConfig = $buildDir->join('velox.toml'); - - \copy($sourceConfig->__toString(), $targetConfig->__toString()) or throw new ConfigException( - "Failed to copy config file from `{$sourceConfig}` to `{$targetConfig}`", - configPath: $config->configFile, - ); - - $this->logger->debug('Copied config file to: %s', (string) $targetConfig); - - return $targetConfig; + Validator::validate($config); } /** From c5110b151e591b39342545149c67156940a36e98 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 15:02:42 +0400 Subject: [PATCH 28/38] refactor(velox): implement pipeline-based configuration processing with multiple sources --- .../Internal/Config/ConfigPipelineBuilder.php | 50 +++ .../Config/Pipeline/ConfigContext.php | 44 ++ .../Config/Pipeline/ConfigPipeline.php | 34 ++ .../Config/Pipeline/ConfigProcessor.php | 26 ++ .../Processor/BaseTemplateProcessor.php | 38 ++ .../Processor/BuildMixinsProcessor.php | 39 ++ .../Processor/GitHubTokenProcessor.php | 39 ++ .../Pipeline/Processor/LocalFileProcessor.php | 52 +++ .../Pipeline/Processor/RemoteApiProcessor.php | 47 ++ .../{TomlMerger.php => Pipeline/TomlData.php} | 134 +++--- .../Velox/Internal/Config/Strategy/Hybrid.php | 52 --- .../Velox/Internal/Config/Strategy/Local.php | 57 --- .../Velox/Internal/Config/Strategy/Remote.php | 42 -- src/Module/Velox/Internal/ConfigBuilder.php | 45 +- .../Internal/Config/Pipeline/TomlDataTest.php | 415 ++++++++++++++++++ 15 files changed, 874 insertions(+), 240 deletions(-) create mode 100644 src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/ConfigPipeline.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/ConfigProcessor.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/Processor/BaseTemplateProcessor.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php create mode 100644 src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php rename src/Module/Velox/Internal/Config/{TomlMerger.php => Pipeline/TomlData.php} (70%) delete mode 100644 src/Module/Velox/Internal/Config/Strategy/Hybrid.php delete mode 100644 src/Module/Velox/Internal/Config/Strategy/Local.php delete mode 100644 src/Module/Velox/Internal/Config/Strategy/Remote.php create mode 100644 tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php diff --git a/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php b/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php new file mode 100644 index 0000000..8c80058 --- /dev/null +++ b/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php @@ -0,0 +1,50 @@ + 1. RemoteAPI -> 2. LocalFile -> 3. BuildMixins -> 4. GitHubToken + * + * @internal + * @psalm-internal Internal\DLoad\Module\Velox + */ +final class ConfigPipelineBuilder +{ + /** + * @param list> $pipes Processors to be used in the pipeline + */ + public function __construct( + private readonly Container $container, + private readonly array $pipes = [ + BaseTemplateProcessor::class, + // RemoteApiProcessor::class, + LocalFileProcessor::class, + BuildMixinsProcessor::class, + GitHubTokenProcessor::class, + ], + ) {} + + public function build(): ConfigPipeline + { + $processors = \array_map( + fn(string $pipe): ConfigProcessor => $this->container->get($pipe), + $this->pipes, + ); + + return new ConfigPipeline($processors); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php new file mode 100644 index 0000000..eb23746 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php @@ -0,0 +1,44 @@ +action, $tomlData, $this->metadata, $this->buildDir); + } + + public function withMetadata(array $metadata): self + { + return new self($this->action, $this->tomlData, $metadata, $this->buildDir); + } + + public function addMetadata(string $key, mixed $value): self + { + $metadata = $this->metadata; + $metadata[$key] = $value; + return $this->withMetadata($metadata); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/ConfigPipeline.php b/src/Module/Velox/Internal/Config/Pipeline/ConfigPipeline.php new file mode 100644 index 0000000..81d4495 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/ConfigPipeline.php @@ -0,0 +1,34 @@ + $processors + */ + public function __construct( + private readonly array $processors, + ) {} + + public function process(ConfigContext $context): ConfigContext + { + return \array_reduce( + $this->processors, + static fn(ConfigContext $ctx, ConfigProcessor $processor): ConfigContext => $processor($ctx), + $context, + ); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/ConfigProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/ConfigProcessor.php new file mode 100644 index 0000000..3b988ab --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/ConfigProcessor.php @@ -0,0 +1,26 @@ + [ + 'level' => 'debug', + 'mode' => 'dev', + ], + 'debug' => [ + 'enabled ' => true, + ], + ]); + + return $context->withTomlData($baseTemplate) + ->addMetadata('base_template_applied', true); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php new file mode 100644 index 0000000..16693c8 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php @@ -0,0 +1,39 @@ +tomlData; + $appliedMixins = []; + + if ($context->action->binaryVersion !== null) { + $tomlData = $tomlData->set('roadrunner.ref', $context->action->binaryVersion); + $appliedMixins[] = 'binary_version'; + } + + if ($appliedMixins === []) { + return $context; + } + + return $context->withTomlData($tomlData) + ->addMetadata('build_mixins_applied', true) + ->addMetadata('applied_mixins', $appliedMixins); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php new file mode 100644 index 0000000..e731aa0 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php @@ -0,0 +1,39 @@ +gitHub->token === null) { + return $context; + } + + $tomlData = $context->tomlData->set('github.token', $this->gitHub->token); + + return $context + ->withTomlData($tomlData) + ->addMetadata('github_token_applied', true); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php new file mode 100644 index 0000000..a89912e --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php @@ -0,0 +1,52 @@ +configFile is not null. + * + * @internal + * @psalm-internal Internal\DLoad\Module\Velox + */ +final class LocalFileProcessor implements ConfigProcessor +{ + public function __invoke(ConfigContext $context): ConfigContext + { + // Early return if no config file + if ($context->action->configFile === null) { + return $context; + } + + $configPath = Path::create($context->action->configFile); + + if (!\file_exists($configPath->__toString())) { + throw new ConfigException( + "Local config file not found: {$configPath}", + ); + } + + $localToml = \file_get_contents($configPath->__toString()); + + $localToml === false and throw new ConfigException( + "Failed to read local config file: {$configPath}.", + ); + + $localData = TomlData::fromString($localToml); + $mergedData = $context->tomlData->merge($localData); + + return $context->withTomlData($mergedData) + ->addMetadata('local_file_applied', true) + ->addMetadata('local_file_path', $configPath->__toString()); + } +} diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php new file mode 100644 index 0000000..bbeba96 --- /dev/null +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php @@ -0,0 +1,47 @@ +plugins is not empty. + * + * @internal + * @psalm-internal Internal\DLoad\Module\Velox + */ +final class RemoteApiProcessor implements ConfigProcessor +{ + public function __construct( + private readonly ApiClient $apiClient, + ) {} + + public function __invoke(ConfigContext $context): ConfigContext + { + // Early return if no plugins + if ($context->action->plugins === []) { + return $context; + } + + $apiToml = $this->apiClient->generateConfig( + $context->action->plugins, + $context->action->golangVersion, + $context->action->binaryVersion, + ); + + $apiData = TomlData::fromString($apiToml); + $mergedData = $context->tomlData->merge($apiData); + + return $context->withTomlData($mergedData) + ->addMetadata('remote_api_applied', true) + ->addMetadata('plugin_count', \count($context->action->plugins)); + } +} diff --git a/src/Module/Velox/Internal/Config/TomlMerger.php b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php similarity index 70% rename from src/Module/Velox/Internal/Config/TomlMerger.php rename to src/Module/Velox/Internal/Config/Pipeline/TomlData.php index c07df07..667033f 100644 --- a/src/Module/Velox/Internal/Config/TomlMerger.php +++ b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php @@ -2,54 +2,66 @@ declare(strict_types=1); -namespace Internal\DLoad\Module\Velox\Internal\Config; - -use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; +namespace Internal\DLoad\Module\Velox\Internal\Config\Pipeline; /** - * Simple TOML merger for combining local and remote configurations. + * Immutable TOML data container for pipeline processing. + * + * Provides methods for merging, setting values, and converting + * between array and TOML string representations. + * Consolidates all TOML parsing and formatting functionality. * * @internal * @psalm-internal Internal\DLoad\Module\Velox */ -final class TomlMerger +final class TomlData { + public function __construct( + private readonly array $data = [], + ) {} + + public static function fromString(string $toml): self + { + $instance = new self(); + $data = $instance->parseToml($toml); + return new self($data); + } + /** - * Merges remote TOML configuration into a local base configuration. - * - * Remote plugins extend/override local plugins while preserving other sections. + * Merges remote TOML configuration into local base configuration. * * @param string $localToml Base TOML configuration * @param string $remoteToml Remote TOML configuration with plugins * @return string Merged TOML configuration - * @throws ConfigException When merging fails */ - public function merge(string $localToml, string $remoteToml): string + public static function mergeTomlStrings(string $localToml, string $remoteToml): string { - try { - $localData = $this->parseToml($localToml); - $remoteData = $this->parseToml($remoteToml); - - // Merge github.plugins sections - if (isset($remoteData['github']['plugins'])) { - $localData['github']['plugins'] = \array_merge( - $localData['github']['plugins'] ?? [], - $remoteData['github']['plugins'], - ); - } + $local = self::fromString($localToml); + $remote = self::fromString($remoteToml); + return $local->merge($remote)->toToml(); + } - // Preserve roadrunner version from local unless remote specifies one - if (isset($remoteData['roadrunner']['ref']) && !isset($localData['roadrunner']['ref'])) { - $localData['roadrunner']['ref'] = $remoteData['roadrunner']['ref']; - } + public function merge(TomlData $other): self + { + $merged = $this->deepMerge($this->data, $other->data); + return new self($merged); + } - return $this->arrayToToml($localData); - } catch (\Throwable $e) { - throw new ConfigException( - 'Failed to merge TOML configurations: ' . $e->getMessage(), - previous: $e, - ); - } + public function set(string $path, mixed $value): self + { + $data = $this->data; + $this->setNestedValue($data, $path, $value); + return new self($data); + } + + public function toToml(): string + { + return $this->arrayToToml($this->data); + } + + public function getData(): array + { + return $this->data; } /** @@ -94,31 +106,6 @@ private function parseToml(string $toml): array return $result; } - /** - * Sets a nested array value using dot notation. - * - * @param array $array Target array - * @param string $path Dot-separated path - * @param mixed $value Value to set - */ - private function setNestedValue(array &$array, string $path, mixed $value): void - { - $keys = \explode('.', $path); - $current = &$array; - - foreach ($keys as $key) { - if (!\is_array($current)) { - $current = []; - } - if (!isset($current[$key])) { - $current[$key] = []; - } - $current = &$current[$key]; - } - - $current = $value; - } - /** * Converts array back to TOML format. * @@ -196,4 +183,37 @@ private function sectionToToml(string $sectionName, array $data): string return $toml; } + + private function deepMerge(array $array1, array $array2): array + { + $merged = $array1; + + foreach ($array2 as $key => $value) { + if (\is_array($value) && isset($merged[$key]) && \is_array($merged[$key])) { + $merged[$key] = $this->deepMerge($merged[$key], $value); + } else { + $merged[$key] = $value; + } + } + + return $merged; + } + + private function setNestedValue(array &$array, string $path, mixed $value): void + { + $keys = \explode('.', $path); + $current = &$array; + + foreach ($keys as $key) { + if (!\is_array($current)) { + $current = []; + } + if (!isset($current[$key])) { + $current[$key] = []; + } + $current = &$current[$key]; + } + + $current = $value; + } } diff --git a/src/Module/Velox/Internal/Config/Strategy/Hybrid.php b/src/Module/Velox/Internal/Config/Strategy/Hybrid.php deleted file mode 100644 index 989710f..0000000 --- a/src/Module/Velox/Internal/Config/Strategy/Hybrid.php +++ /dev/null @@ -1,52 +0,0 @@ -localStrategy = new Local(); - $this->remoteStrategy = new Remote($apiClient); - } - - public function supports(VeloxAction $action): bool - { - return $action->configFile !== null && $action->plugins !== []; - } - - public function build(VeloxAction $action): string - { - $this->supports($action) or throw new ConfigException( - 'Hybrid config strategy requires both config file and plugins', - ); - - // Get local config content - $localConfig = $this->localStrategy->build($action); - - // Generate remote config from plugins - $remoteConfig = $this->remoteStrategy->build($action); - - // Merge configurations (remote plugins extend/override local) - return $this->tomlMerger->merge($localConfig, $remoteConfig); - } -} diff --git a/src/Module/Velox/Internal/Config/Strategy/Local.php b/src/Module/Velox/Internal/Config/Strategy/Local.php deleted file mode 100644 index e591bd5..0000000 --- a/src/Module/Velox/Internal/Config/Strategy/Local.php +++ /dev/null @@ -1,57 +0,0 @@ -configFile !== null && $action->plugins === []; - } - - public function build(VeloxAction $action): string - { - $configFile = $action->configFile ?? throw new ConfigException( - 'Local config strategy requires a config file', - ); - - $configPath = Path::create($configFile); - - $configPath->exists() or throw new ConfigException( - "Config file not found: {$configFile}", - configPath: $configFile, - ); - - $configPath->isFile() or throw new ConfigException( - "Config path is not a file: {$configFile}", - configPath: $configFile, - ); - - $configPath->isReadable() or throw new ConfigException( - "Config file is not readable: {$configFile}", - configPath: $configFile, - ); - - $content = \file_get_contents($configPath->__toString()); - - $content !== false or throw new ConfigException( - "Failed to read config file: {$configFile}", - configPath: $configFile, - ); - - return $content; - } -} diff --git a/src/Module/Velox/Internal/Config/Strategy/Remote.php b/src/Module/Velox/Internal/Config/Strategy/Remote.php deleted file mode 100644 index 3b2070d..0000000 --- a/src/Module/Velox/Internal/Config/Strategy/Remote.php +++ /dev/null @@ -1,42 +0,0 @@ -configFile === null && $action->plugins !== []; - } - - public function build(VeloxAction $action): string - { - $action->plugins !== [] or throw new ConfigException( - 'Remote config strategy requires at least one plugin', - ); - - return $this->apiClient->generateConfig( - plugins: $action->plugins, - golangVersion: $action->golangVersion, - binaryVersion: $action->binaryVersion, - options: $action->options ?? [], - ); - } -} diff --git a/src/Module/Velox/Internal/ConfigBuilder.php b/src/Module/Velox/Internal/ConfigBuilder.php index 466e920..f99dfa0 100644 --- a/src/Module/Velox/Internal/ConfigBuilder.php +++ b/src/Module/Velox/Internal/ConfigBuilder.php @@ -6,13 +6,10 @@ use Internal\DLoad\Module\Common\FileSystem\Path; use Internal\DLoad\Module\Config\Schema\Action\Velox as VeloxAction; -use Internal\DLoad\Module\Velox\ApiClient; use Internal\DLoad\Module\Velox\Exception\Config; use Internal\DLoad\Module\Velox\Exception\Config as ConfigException; -use Internal\DLoad\Module\Velox\Internal\Config\Strategy; -use Internal\DLoad\Module\Velox\Internal\Config\Strategy\Hybrid; -use Internal\DLoad\Module\Velox\Internal\Config\Strategy\Local; -use Internal\DLoad\Module\Velox\Internal\Config\Strategy\Remote; +use Internal\DLoad\Module\Velox\Internal\Config\ConfigPipelineBuilder; +use Internal\DLoad\Module\Velox\Internal\Config\Pipeline\ConfigContext; use Internal\DLoad\Module\Velox\Internal\Config\Validator; use Internal\DLoad\Service\Logger; @@ -20,12 +17,8 @@ * Main Velox configuration builder service. * * Provides functionality to generate velox.toml configuration files - * from various sources (local files, remote API, or hybrid approach). - * - * Uses strategy pattern to handle different configuration scenarios: - * - Local config file only - * - Remote API config only - * - Hybrid (local + remote merge) + * using a pipeline-based architecture that processes multiple configuration + * sources in sequence: base template → remote API → local file → build mixins → GitHub token. * * @internal * @psalm-internal Internal\DLoad\Module\Velox @@ -33,7 +26,7 @@ final class ConfigBuilder { public function __construct( - private readonly ApiClient $apiClient, + private readonly ConfigPipelineBuilder $pipelineBuilder, private readonly Validator $validator, private readonly Logger $logger, ) {} @@ -41,10 +34,8 @@ public function __construct( /** * Builds a velox.toml configuration file for the given action. * - * Handles all configuration scenarios: - * - Local config file only - * - Remote API config only - * - Hybrid (local + remote merge) + * Uses pipeline-based architecture to process configuration sources in sequence: + * 0. Base template → 1. Remote API → 2. Local file → 3. Build mixins → 4. GitHub token * * @param VeloxAction $action Build configuration specification * @param Path $buildDir Directory where config file should be created @@ -53,10 +44,13 @@ public function __construct( */ public function buildConfig(VeloxAction $action, Path $buildDir): Path { - $this->logger->debug('Building Velox configuration...'); + $this->logger->debug('Building Velox configuration with pipeline...'); + + $pipeline = $this->pipelineBuilder->build(); + $context = new ConfigContext($action, buildDir: $buildDir); - $strategy = $this->getConfigStrategy($action); - $configContent = $strategy->build($action); + $result = $pipeline->process($context); + $configContent = $result->tomlData->toToml(); $configPath = $buildDir->join('velox.toml'); @@ -84,17 +78,4 @@ public function validateConfig(Path $configPath): bool { return $this->validator->validateTomlFile($configPath); } - - /** - * Selects the appropriate configuration strategy based on action inputs. - */ - private function getConfigStrategy(VeloxAction $action): Strategy - { - return match (true) { - $action->configFile !== null && $action->plugins !== [] => new Hybrid($this->apiClient), - $action->configFile !== null => new Local(), - $action->plugins !== [] => new Remote($this->apiClient), - default => throw new ConfigException('No valid configuration source provided'), - }; - } } diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php new file mode 100644 index 0000000..74f70e9 --- /dev/null +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php @@ -0,0 +1,415 @@ + [ + "[github.plugins.logger]\ntype = \"logger\"", + ['github' => ['plugins' => ['logger' => ['type' => 'logger']]]], + ]; + + yield 'multiple nested sections' => [ + "[github.plugins.logger]\ntype = \"logger\"\n\n[github.plugins.cache]\ntype = \"cache\"", + [ + 'github' => [ + 'plugins' => [ + 'logger' => ['type' => 'logger'], + 'cache' => ['type' => 'cache'], + ], + ], + ], + ]; + + yield 'deeply nested section' => [ + "[a.b.c.d]\nvalue = \"deep\"", + ['a' => ['b' => ['c' => ['d' => ['value' => 'deep']]]]], + ]; + } + + public static function provideSetPathData(): \Generator + { + yield 'simple key' => [ + 'newkey', + 'newvalue', + ['existing' => 'value', 'newkey' => 'newvalue'], + ]; + + yield 'nested path' => [ + 'section.nested', + 'data', + ['existing' => 'value', 'section' => ['nested' => 'data']], + ]; + + yield 'deeply nested path' => [ + 'a.b.c.d', + 'deep', + ['existing' => 'value', 'a' => ['b' => ['c' => ['d' => 'deep']]]], + ]; + + yield 'overwrite existing top-level key' => [ + 'existing', + 'updated', + ['existing' => 'updated'], + ]; + } + + public static function provideQuotedValues(): \Generator + { + yield 'double quotes' => [ + 'key = "value with spaces"', + ['key' => 'value with spaces'], + ]; + + yield 'single quotes' => [ + "key = 'value with spaces'", + ['key' => 'value with spaces'], + ]; + + yield 'no quotes' => [ + 'key = simple_value', + ['key' => 'simple_value'], + ]; + + yield 'mixed quotes in section' => [ + "[section]\ndouble = \"quoted\"\nsingle = 'quoted'\nbare = unquoted", + [ + 'section' => [ + 'double' => 'quoted', + 'single' => 'quoted', + 'bare' => 'unquoted', + ], + ], + ]; + } + + public function testConstructorCreatesEmptyInstance(): void + { + // Act + $tomlData = new TomlData(); + + // Assert + self::assertSame([], $tomlData->getData()); + } + + public function testConstructorCreatesInstanceWithData(): void + { + // Arrange + $data = ['key' => 'value', 'section' => ['nested' => 'data']]; + + // Act + $tomlData = new TomlData($data); + + // Assert + self::assertSame($data, $tomlData->getData()); + } + + public function testFromStringCreatesInstanceFromTomlString(): void + { + // Arrange + $toml = "key = \"value\"\n\n[section]\nnested = \"data\""; + $expectedData = [ + 'key' => 'value', + 'section' => ['nested' => 'data'], + ]; + + // Act + $tomlData = TomlData::fromString($toml); + + // Assert + self::assertSame($expectedData, $tomlData->getData()); + } + + public function testFromStringHandlesEmptyString(): void + { + // Act + $tomlData = TomlData::fromString(''); + + // Assert + self::assertSame([], $tomlData->getData()); + } + + public function testFromStringHandlesCommentsAndEmptyLines(): void + { + // Arrange + $toml = "# This is a comment\n\nkey = \"value\"\n# Another comment\n\n[section]\n# Comment in section\nnested = \"data\""; + $expectedData = [ + 'key' => 'value', + 'section' => ['nested' => 'data'], + ]; + + // Act + $tomlData = TomlData::fromString($toml); + + // Assert + self::assertSame($expectedData, $tomlData->getData()); + } + + #[DataProvider('provideNestedSectionData')] + public function testFromStringHandlesNestedSections(string $toml, array $expectedData): void + { + // Act + $tomlData = TomlData::fromString($toml); + + // Assert + self::assertSame($expectedData, $tomlData->getData()); + } + + public function testMergeCreatesNewInstanceWithMergedData(): void + { + // Arrange + $data1 = ['key1' => 'value1', 'section' => ['nested1' => 'data1']]; + $data2 = ['key2' => 'value2', 'section' => ['nested2' => 'data2']]; + $tomlData1 = new TomlData($data1); + $tomlData2 = new TomlData($data2); + $expectedMerged = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'section' => [ + 'nested1' => 'data1', + 'nested2' => 'data2', + ], + ]; + + // Act + $merged = $tomlData1->merge($tomlData2); + + // Assert + self::assertNotSame($tomlData1, $merged); + self::assertNotSame($tomlData2, $merged); + self::assertEquals($expectedMerged, $merged->getData()); + } + + public function testMergeOverwritesExistingKeys(): void + { + // Arrange + $data1 = ['key' => 'original', 'section' => ['nested' => 'original']]; + $data2 = ['key' => 'updated', 'section' => ['nested' => 'updated']]; + $tomlData1 = new TomlData($data1); + $tomlData2 = new TomlData($data2); + $expectedMerged = [ + 'key' => 'updated', + 'section' => ['nested' => 'updated'], + ]; + + // Act + $merged = $tomlData1->merge($tomlData2); + + // Assert + self::assertSame($expectedMerged, $merged->getData()); + } + + public function testMergeHandlesNestedArrays(): void + { + // Arrange + $data1 = ['section' => ['key1' => 'value1', 'nested' => ['deep1' => 'data1']]]; + $data2 = ['section' => ['key2' => 'value2', 'nested' => ['deep2' => 'data2']]]; + $tomlData1 = new TomlData($data1); + $tomlData2 = new TomlData($data2); + $expectedMerged = [ + 'section' => [ + 'key1' => 'value1', + 'key2' => 'value2', + 'nested' => [ + 'deep1' => 'data1', + 'deep2' => 'data2', + ], + ], + ]; + + // Act + $merged = $tomlData1->merge($tomlData2); + + // Assert + self::assertEquals($expectedMerged, $merged->getData()); + } + + #[DataProvider('provideSetPathData')] + public function testSetCreatesNewInstanceWithUpdatedValue(string $path, mixed $value, array $expectedData): void + { + // Arrange + $initialData = ['existing' => 'value']; + $tomlData = new TomlData($initialData); + + // Act + $updated = $tomlData->set($path, $value); + + // Assert + self::assertNotSame($tomlData, $updated); + self::assertSame($expectedData, $updated->getData()); + self::assertSame($initialData, $tomlData->getData()); // Original unchanged + } + + public function testToTomlConvertsDataToTomlString(): void + { + // Arrange + $data = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'section' => [ + 'nested1' => 'data1', + 'nested2' => 'data2', + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testToTomlHandlesNestedSections(): void + { + // Arrange + $data = [ + 'github' => [ + 'plugins' => [ + 'logger' => 'enabled', + 'cache' => 'disabled', + ], + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = "[github.plugins]\nlogger = \"enabled\"\ncache = \"disabled\"\n\n"; + + // Act + $result = $tomlData->toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testToTomlHandlesMixedSectionTypes(): void + { + // Arrange + $data = [ + 'roadrunner' => [ + 'simple' => 'value', + 'plugins' => ['logger' => 'enabled'], + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testToTomlHandlesEmptyData(): void + { + // Arrange + $tomlData = new TomlData(); + + // Act + $result = $tomlData->toToml(); + + // Assert + self::assertSame('', $result); + } + + public function testMergeTomlStringsReturnsMergedTomlString(): void + { + // Arrange + $localToml = "local_key = \"local_value\"\n\n[roadrunner]\nversion = \"1.0\""; + $remoteToml = "remote_key = \"remote_value\"\n\n[github.plugins]\nlogger = \"enabled\""; + $expectedMerged = "local_key = \"local_value\"\nremote_key = \"remote_value\"\n\n[roadrunner]\nversion = \"1.0\"\n\n[github.plugins]\nlogger = \"enabled\"\n\n"; + + // Act + $result = TomlData::mergeTomlStrings($localToml, $remoteToml); + + // Assert + self::assertSame($expectedMerged, $result); + } + + public function testMergeTomlStringsHandlesEmptyStrings(): void + { + // Arrange + $localToml = "key = \"value\""; + $emptyToml = ""; + + // Act + $result1 = TomlData::mergeTomlStrings($localToml, $emptyToml); + $result2 = TomlData::mergeTomlStrings($emptyToml, $localToml); + + // Assert + self::assertSame("key = \"value\"\n\n", $result1); + self::assertSame("key = \"value\"\n\n", $result2); + } + + public function testRoundTripConversion(): void + { + // Arrange + $originalToml = "key = \"value\"\n\n[section]\nnested = \"data\""; + + // Act + $tomlData = TomlData::fromString($originalToml); + $convertedToml = $tomlData->toToml(); + $roundTripData = TomlData::fromString($convertedToml); + + // Assert + self::assertSame($tomlData->getData(), $roundTripData->getData()); + } + + #[DataProvider('provideQuotedValues')] + public function testFromStringHandlesQuotedValues(string $toml, array $expectedData): void + { + // Act + $tomlData = TomlData::fromString($toml); + + // Assert + self::assertSame($expectedData, $tomlData->getData()); + } + + public function testImmutabilityOfOriginalData(): void + { + // Arrange + $originalData = ['key' => 'value']; + $tomlData = new TomlData($originalData); + + // Act + $tomlData->set('newkey', 'newvalue'); + $tomlData->merge(new TomlData(['otherkey' => 'othervalue'])); + + // Assert - original data and instance should be unchanged + self::assertSame(['key' => 'value'], $tomlData->getData()); + self::assertSame(['key' => 'value'], $originalData); + } + + public function testGetDataReturnsReadOnlyArray(): void + { + // Arrange + $tomlData = new TomlData(['key' => 'value']); + + // Act + $data = $tomlData->getData(); + + // Assert + self::assertSame(['key' => 'value'], $data); + } +} From bc12c03c4c76b1621fdab6f73ab70205f9ebe5cc Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 15:06:02 +0400 Subject: [PATCH 29/38] test(velox): add unit tests for `ConfigPipeline` processing and context handling --- .../Config/Pipeline/ConfigPipelineTest.php | 383 ++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php new file mode 100644 index 0000000..5671f9b --- /dev/null +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php @@ -0,0 +1,383 @@ +createMock(ConfigProcessor::class); + $processor2 = $this->createMock(ConfigProcessor::class); + $processors = [$processor1, $processor2]; + + // Act + $pipeline = new ConfigPipeline($processors); + + // Assert + self::assertInstanceOf(ConfigPipeline::class, $pipeline); + } + + public function testConstructorCreatesInstanceWithEmptyProcessors(): void + { + // Arrange + $processors = []; + + // Act + $pipeline = new ConfigPipeline($processors); + + // Assert + self::assertInstanceOf(ConfigPipeline::class, $pipeline); + } + + public function testProcessWithEmptyProcessorsReturnsOriginalContext(): void + { + // Arrange + $processors = []; + $pipeline = new ConfigPipeline($processors); + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['key' => 'value']), + ['metadata' => 'test'], + $this->buildDir, + ); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($originalContext, $result); + } + + public function testProcessWithSingleProcessorCallsProcessor(): void + { + // Arrange + $processor = $this->createMock(ConfigProcessor::class); + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['key' => 'value']), + [], + $this->buildDir, + ); + $modifiedContext = new ConfigContext( + $this->veloxAction, + new TomlData(['key' => 'modified']), + [], + $this->buildDir, + ); + + $processor->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($modifiedContext); + + $pipeline = new ConfigPipeline([$processor]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($modifiedContext, $result); + self::assertNotSame($originalContext, $result); + } + + public function testProcessWithMultipleProcessorsCallsThemInSequence(): void + { + // Arrange + $processor1 = $this->createMock(ConfigProcessor::class); + $processor2 = $this->createMock(ConfigProcessor::class); + $processor3 = $this->createMock(ConfigProcessor::class); + + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['step' => '0']), + [], + $this->buildDir, + ); + $context1 = new ConfigContext( + $this->veloxAction, + new TomlData(['step' => '1']), + [], + $this->buildDir, + ); + $context2 = new ConfigContext( + $this->veloxAction, + new TomlData(['step' => '2']), + [], + $this->buildDir, + ); + $finalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['step' => '3']), + [], + $this->buildDir, + ); + + // Set up expectations for sequential processing + $processor1->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($context1); + + $processor2->expects(self::once()) + ->method('__invoke') + ->with($context1) + ->willReturn($context2); + + $processor3->expects(self::once()) + ->method('__invoke') + ->with($context2) + ->willReturn($finalContext); + + $pipeline = new ConfigPipeline([$processor1, $processor2, $processor3]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($finalContext, $result); + self::assertSame(['step' => '3'], $result->tomlData->getData()); + } + + public function testProcessPassesThroughComplexContextChanges(): void + { + // Arrange + $processor1 = $this->createMock(ConfigProcessor::class); + $processor2 = $this->createMock(ConfigProcessor::class); + + $originalTomlData = new TomlData(['initial' => 'data']); + $originalMetadata = ['version' => '1.0']; + $originalContext = new ConfigContext( + $this->veloxAction, + $originalTomlData, + $originalMetadata, + $this->buildDir, + ); + + // First processor modifies TOML data + $intermediateTomlData = new TomlData(['initial' => 'data', 'added_by_p1' => 'value1']); + $intermediateContext = new ConfigContext( + $this->veloxAction, + $intermediateTomlData, + $originalMetadata, + $this->buildDir, + ); + + // Second processor modifies metadata + $finalMetadata = ['version' => '1.0', 'processed_by' => 'p2']; + $finalContext = new ConfigContext( + $this->veloxAction, + $intermediateTomlData, + $finalMetadata, + $this->buildDir, + ); + + $processor1->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($intermediateContext); + + $processor2->expects(self::once()) + ->method('__invoke') + ->with($intermediateContext) + ->willReturn($finalContext); + + $pipeline = new ConfigPipeline([$processor1, $processor2]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($finalContext, $result); + self::assertSame(['initial' => 'data', 'added_by_p1' => 'value1'], $result->tomlData->getData()); + self::assertSame(['version' => '1.0', 'processed_by' => 'p2'], $result->metadata); + } + + public function testProcessPreservesContextImmutability(): void + { + // Arrange + $processor = $this->createMock(ConfigProcessor::class); + $originalTomlData = new TomlData(['original' => 'data']); + $originalMetadata = ['original' => 'metadata']; + $originalContext = new ConfigContext( + $this->veloxAction, + $originalTomlData, + $originalMetadata, + $this->buildDir, + ); + + $modifiedContext = new ConfigContext( + $this->veloxAction, + new TomlData(['modified' => 'data']), + ['modified' => 'metadata'], + $this->buildDir, + ); + + $processor->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($modifiedContext); + + $pipeline = new ConfigPipeline([$processor]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert - Original context should remain unchanged + self::assertSame(['original' => 'data'], $originalContext->tomlData->getData()); + self::assertSame(['original' => 'metadata'], $originalContext->metadata); + + // Result should have modified data + self::assertSame(['modified' => 'data'], $result->tomlData->getData()); + self::assertSame(['modified' => 'metadata'], $result->metadata); + } + + public function testProcessWithProcessorThatReturnsUnchangedContext(): void + { + // Arrange + $processor1 = $this->createMock(ConfigProcessor::class); + $processor2 = $this->createMock(ConfigProcessor::class); + $processor3 = $this->createMock(ConfigProcessor::class); + + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['data' => 'original']), + [], + $this->buildDir, + ); + + $modifiedContext = new ConfigContext( + $this->veloxAction, + new TomlData(['data' => 'modified_by_p1']), + [], + $this->buildDir, + ); + + $finalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['data' => 'modified_by_p3']), + [], + $this->buildDir, + ); + + // First processor modifies context + $processor1->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($modifiedContext); + + // Second processor returns context unchanged (simulating conditional processing) + $processor2->expects(self::once()) + ->method('__invoke') + ->with($modifiedContext) + ->willReturn($modifiedContext); + + // Third processor modifies context again + $processor3->expects(self::once()) + ->method('__invoke') + ->with($modifiedContext) + ->willReturn($finalContext); + + $pipeline = new ConfigPipeline([$processor1, $processor2, $processor3]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($finalContext, $result); + self::assertSame(['data' => 'modified_by_p3'], $result->tomlData->getData()); + } + + public function testProcessMaintainsActionAndBuildDirThroughPipeline(): void + { + // Arrange + $processor = $this->createMock(ConfigProcessor::class); + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['test' => 'data']), + ['test' => 'metadata'], + $this->buildDir, + ); + + // Processor only modifies TOML data and metadata, not action or buildDir + $modifiedContext = new ConfigContext( + $this->veloxAction, + new TomlData(['modified' => 'data']), + ['modified' => 'metadata'], + $this->buildDir, + ); + + $processor->expects(self::once()) + ->method('__invoke') + ->with($originalContext) + ->willReturn($modifiedContext); + + $pipeline = new ConfigPipeline([$processor]); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame($this->veloxAction, $result->action); + self::assertSame($this->buildDir, $result->buildDir); + self::assertSame(['modified' => 'data'], $result->tomlData->getData()); + self::assertSame(['modified' => 'metadata'], $result->metadata); + } + + public function testProcessHandlesLargeNumberOfProcessors(): void + { + // Arrange + $processors = []; + $expectedValue = 0; + + // Create 10 processors that each increment a counter in the TOML data + for ($i = 0; $i < 10; $i++) { + $processor = $this->createMock(ConfigProcessor::class); + $expectedValue = $i + 1; + + $processor->expects(self::once()) + ->method('__invoke') + ->willReturnCallback(static function (ConfigContext $context) use ($expectedValue): ConfigContext { + return $context->withTomlData(new TomlData(['counter' => $expectedValue])); + }); + + $processors[] = $processor; + } + + $originalContext = new ConfigContext( + $this->veloxAction, + new TomlData(['counter' => 0]), + [], + $this->buildDir, + ); + + $pipeline = new ConfigPipeline($processors); + + // Act + $result = $pipeline->process($originalContext); + + // Assert + self::assertSame(['counter' => 10], $result->tomlData->getData()); + } + + protected function setUp(): void + { + $this->veloxAction = new VeloxAction(); + $this->buildDir = Path::create('/tmp/build'); + } +} From f2989cc0b15aedf27f38109b45775960cb039bf8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 16:24:51 +0400 Subject: [PATCH 30/38] refactor(velox): reorder parameters in ConfigContext constructor and related methods --- .../Config/Pipeline/ConfigContext.php | 6 ++-- .../Config/Pipeline/ConfigPipelineTest.php | 36 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php index eb23746..c3d3fae 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php +++ b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php @@ -20,19 +20,19 @@ final class ConfigContext { public function __construct( public readonly VeloxAction $action, + public readonly Path $buildDir, public readonly TomlData $tomlData = new TomlData(), public readonly array $metadata = [], - public readonly Path $buildDir, ) {} public function withTomlData(TomlData $tomlData): self { - return new self($this->action, $tomlData, $this->metadata, $this->buildDir); + return new self($this->action, $this->buildDir, $tomlData, $this->metadata); } public function withMetadata(array $metadata): self { - return new self($this->action, $this->tomlData, $metadata, $this->buildDir); + return new self($this->action, $this->buildDir, $this->tomlData, $metadata); } public function addMetadata(string $key, mixed $value): self diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php index 5671f9b..d2ff963 100644 --- a/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/ConfigPipelineTest.php @@ -52,9 +52,9 @@ public function testProcessWithEmptyProcessorsReturnsOriginalContext(): void $pipeline = new ConfigPipeline($processors); $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['key' => 'value']), ['metadata' => 'test'], - $this->buildDir, ); // Act @@ -70,15 +70,15 @@ public function testProcessWithSingleProcessorCallsProcessor(): void $processor = $this->createMock(ConfigProcessor::class); $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['key' => 'value']), [], - $this->buildDir, ); $modifiedContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['key' => 'modified']), [], - $this->buildDir, ); $processor->expects(self::once()) @@ -105,27 +105,27 @@ public function testProcessWithMultipleProcessorsCallsThemInSequence(): void $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['step' => '0']), [], - $this->buildDir, ); $context1 = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['step' => '1']), [], - $this->buildDir, ); $context2 = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['step' => '2']), [], - $this->buildDir, ); $finalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['step' => '3']), [], - $this->buildDir, ); // Set up expectations for sequential processing @@ -164,27 +164,27 @@ public function testProcessPassesThroughComplexContextChanges(): void $originalMetadata = ['version' => '1.0']; $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, $originalTomlData, $originalMetadata, - $this->buildDir, ); // First processor modifies TOML data $intermediateTomlData = new TomlData(['initial' => 'data', 'added_by_p1' => 'value1']); $intermediateContext = new ConfigContext( $this->veloxAction, + $this->buildDir, $intermediateTomlData, $originalMetadata, - $this->buildDir, ); // Second processor modifies metadata $finalMetadata = ['version' => '1.0', 'processed_by' => 'p2']; $finalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, $intermediateTomlData, $finalMetadata, - $this->buildDir, ); $processor1->expects(self::once()) @@ -216,16 +216,16 @@ public function testProcessPreservesContextImmutability(): void $originalMetadata = ['original' => 'metadata']; $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, $originalTomlData, $originalMetadata, - $this->buildDir, ); $modifiedContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['modified' => 'data']), ['modified' => 'metadata'], - $this->buildDir, ); $processor->expects(self::once()) @@ -256,23 +256,23 @@ public function testProcessWithProcessorThatReturnsUnchangedContext(): void $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['data' => 'original']), [], - $this->buildDir, ); $modifiedContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['data' => 'modified_by_p1']), [], - $this->buildDir, ); $finalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['data' => 'modified_by_p3']), [], - $this->buildDir, ); // First processor modifies context @@ -309,17 +309,17 @@ public function testProcessMaintainsActionAndBuildDirThroughPipeline(): void $processor = $this->createMock(ConfigProcessor::class); $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['test' => 'data']), ['test' => 'metadata'], - $this->buildDir, ); // Processor only modifies TOML data and metadata, not action or buildDir $modifiedContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['modified' => 'data']), ['modified' => 'metadata'], - $this->buildDir, ); $processor->expects(self::once()) @@ -361,9 +361,9 @@ public function testProcessHandlesLargeNumberOfProcessors(): void $originalContext = new ConfigContext( $this->veloxAction, + $this->buildDir, new TomlData(['counter' => 0]), [], - $this->buildDir, ); $pipeline = new ConfigPipeline($processors); From e8b66e1d7c3b60d639c6b5e6f8d75d7d9a225077 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 16:39:29 +0400 Subject: [PATCH 31/38] test(velox): add unit tests for LocalFileProcessor handling TOML configuration files --- .../Processor/LocalFileProcessorTest.php | 371 ++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 tests/Unit/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessorTest.php diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessorTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessorTest.php new file mode 100644 index 0000000..0d49907 --- /dev/null +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessorTest.php @@ -0,0 +1,371 @@ + [ + 'binary_name = "test-binary"', + ['binary_name' => 'test-binary'], + ]; + + yield 'section with nested values' => [ + '[roadrunner]' . "\n" . 'version = "latest"', + ['roadrunner' => null], // Just verify the key exists + ]; + + yield 'empty file' => [ + '', + ['_empty' => null], // Add assertion to avoid risky test + ]; + + yield 'file with comments' => [ + '# This is a comment' . "\n" . 'key = "value"', + ['key' => 'value'], + ]; + + yield 'complex nested structure' => [ + '[github.plugins.http]' . "\n" . 'ref = "v4.7.0"' . "\n" . + '[github.plugins.logger]' . "\n" . 'ref = "v1.2.3"', + ['github' => null], + ]; + } + + public function testInvokeReturnsOriginalContextWhenConfigFileIsNull(): void + { + // Arrange + $this->veloxAction->configFile = null; + $originalContext = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(['base' => 'data']), + ['initial' => 'metadata'], + ); + + // Act + $result = $this->processor->__invoke($originalContext); + + // Assert + self::assertSame($originalContext, $result); + } + + public function testInvokeThrowsExceptionWhenConfigFileDoesNotExist(): void + { + // Arrange + $configPath = '/non/existent/path.toml'; + $this->veloxAction->configFile = $configPath; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + [], + ); + + // Assert (before Act for exceptions) + $this->expectException(ConfigException::class); + $this->expectExceptionMessage("Local config file not found: {$configPath}"); + + // Act + $this->processor->__invoke($context); + } + + public function testInvokeThrowsExceptionWhenFileCannotBeRead(): void + { + // Skip this test on Windows as chmod doesn't work the same way + if (PHP_OS_FAMILY === 'Windows') { + self::markTestSkipped('File permission tests are not reliable on Windows'); + } + + // Arrange + $tempFile = $this->createTempFile('test content'); + $this->veloxAction->configFile = $tempFile; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + [], + ); + + // Make file unreadable by changing permissions + \chmod($tempFile, 0000); + + // Assert (before Act for exceptions) + $this->expectException(ConfigException::class); + $this->expectExceptionMessage("Failed to read local config file: {$tempFile}"); + + // Act + try { + $this->processor->__invoke($context); + } finally { + // Clean up - restore permissions before deletion + \chmod($tempFile, 0644); + \unlink($tempFile); + } + } + + public function testInvokeSuccessfullyProcessesValidTomlFile(): void + { + // Arrange + $tomlContent = 'binary_name = "custom-roadrunner"' . "\n" . + '[github.plugins.logger]' . "\n" . + 'ref = "master"'; + $tempFile = $this->createTempFile($tomlContent); + + $this->veloxAction->configFile = $tempFile; + $baseData = new TomlData(['existing' => 'base_data']); + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + $baseData, + ['original' => 'metadata'], + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + self::assertNotSame($context, $result); + + $resultData = $result->tomlData->getData(); + self::assertSame('base_data', $resultData['existing']); + self::assertSame('custom-roadrunner', $resultData['binary_name']); + self::assertSame('master', $resultData['github']['plugins']['logger']['ref']); + + self::assertTrue($result->metadata['local_file_applied']); + self::assertSame(\str_replace('\\', '/', $tempFile), $result->metadata['local_file_path']); + self::assertSame('metadata', $result->metadata['original']); + + // Clean up + \unlink($tempFile); + } + + public function testInvokeMergesLocalDataWithExistingData(): void + { + // Arrange + $tomlContent = '[roadrunner]' . "\n" . + 'version = "2023.3.0"' . "\n" . + '[github.plugins.http]' . "\n" . + 'ref = "v4.7.0"'; + $tempFile = $this->createTempFile($tomlContent); + + $this->veloxAction->configFile = $tempFile; + $baseData = new TomlData([ + 'roadrunner' => ['binary' => 'rr'], + 'github' => ['plugins' => ['logger' => ['ref' => 'v1.0.0']]], + ]); + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + $baseData, + [], + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + $resultData = $result->tomlData->getData(); + + // Verify merge behavior + self::assertSame('rr', $resultData['roadrunner']['binary']); + self::assertSame('2023.3.0', $resultData['roadrunner']['version']); + self::assertSame('v1.0.0', $resultData['github']['plugins']['logger']['ref']); + self::assertSame('v4.7.0', $resultData['github']['plugins']['http']['ref']); + + // Clean up + \unlink($tempFile); + } + + #[DataProvider('provideValidTomlFiles')] + public function testInvokeHandlesVariousTomlFormats( + string $tomlContent, + array $expectedKeys, + ): void { + // Arrange + $tempFile = $this->createTempFile($tomlContent); + $this->veloxAction->configFile = $tempFile; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + [], + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + $resultData = $result->tomlData->getData(); + + if (\array_key_exists('_empty', $expectedKeys)) { + // Special case for empty file test + self::assertEmpty($resultData); + } else { + foreach ($expectedKeys as $key => $expectedValue) { + self::assertArrayHasKey($key, $resultData); + if ($expectedValue !== null) { + self::assertSame($expectedValue, $resultData[$key]); + } + } + } + + // Clean up + \unlink($tempFile); + } + + public function testInvokePreservesContextImmutability(): void + { + // Arrange + $tomlContent = 'new_key = "new_value"'; + $tempFile = $this->createTempFile($tomlContent); + + $this->veloxAction->configFile = $tempFile; + $originalData = new TomlData(['original' => 'data']); + $originalMetadata = ['original' => 'metadata']; + $originalContext = new ConfigContext( + $this->veloxAction, + $this->buildDir, + $originalData, + $originalMetadata, + ); + + // Act + $result = $this->processor->__invoke($originalContext); + + // Assert - Original context should remain unchanged + self::assertSame(['original' => 'data'], $originalContext->tomlData->getData()); + self::assertSame(['original' => 'metadata'], $originalContext->metadata); + self::assertSame($this->veloxAction, $originalContext->action); + self::assertSame($this->buildDir, $originalContext->buildDir); + + // Result should have new data + $resultData = $result->tomlData->getData(); + self::assertSame('data', $resultData['original']); + self::assertSame('new_value', $resultData['new_key']); + self::assertTrue($result->metadata['local_file_applied']); + + // Clean up + \unlink($tempFile); + } + + public function testInvokePreservesActionAndBuildDir(): void + { + // Arrange + $tomlContent = 'test = "value"'; + $tempFile = $this->createTempFile($tomlContent); + + $this->veloxAction->configFile = $tempFile; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + [], + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + self::assertSame($this->veloxAction, $result->action); + self::assertSame($this->buildDir, $result->buildDir); + + // Clean up + \unlink($tempFile); + } + + public function testInvokeAddsCorrectMetadata(): void + { + // Arrange + $tomlContent = 'test_key = "test_value"'; + $tempFile = $this->createTempFile($tomlContent); + + $this->veloxAction->configFile = $tempFile; + $originalMetadata = ['existing' => 'value', 'count' => 42]; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + $originalMetadata, + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + self::assertTrue($result->metadata['local_file_applied']); + self::assertSame(\str_replace('\\', '/', $tempFile), $result->metadata['local_file_path']); + + // Verify existing metadata is preserved + self::assertSame('value', $result->metadata['existing']); + self::assertSame(42, $result->metadata['count']); + + // Clean up + \unlink($tempFile); + } + + public function testInvokeWithRelativeConfigPath(): void + { + // Arrange + $tomlContent = 'relative_test = "success"'; + $tempFile = $this->createTempFile($tomlContent); + $relativePath = \basename($tempFile); + + // Change to temp directory to make relative path work + $originalDir = \getcwd(); + \chdir(\dirname($tempFile)); + + $this->veloxAction->configFile = $relativePath; + $context = new ConfigContext( + $this->veloxAction, + $this->buildDir, + new TomlData(), + [], + ); + + // Act + $result = $this->processor->__invoke($context); + + // Assert + $resultData = $result->tomlData->getData(); + self::assertSame('success', $resultData['relative_test']); + self::assertTrue($result->metadata['local_file_applied']); + + // Clean up + \chdir($originalDir); + \unlink($tempFile); + } + + protected function setUp(): void + { + // Arrange (common setup) + $this->processor = new LocalFileProcessor(); + $this->veloxAction = new VeloxAction(); + $this->buildDir = Path::create('/tmp/build'); + } + + private function createTempFile(string $content): string + { + $tempFile = \tempnam(\sys_get_temp_dir(), 'test_velox_'); + \file_put_contents($tempFile, $content); + return $tempFile; + } +} From 7c71d43ca5c9e092202a17c56f72bf1f1af5895e Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 16:59:15 +0400 Subject: [PATCH 32/38] feat(velox): enhance TOML serialization with support for nested sections and inline arrays --- .../Internal/Config/Pipeline/TomlData.php | 159 +++++++++++-- src/Module/Velox/Internal/ConfigBuilder.php | 2 +- .../Internal/Config/Pipeline/TomlDataTest.php | 224 +++++++++++++++++- 3 files changed, 356 insertions(+), 29 deletions(-) diff --git a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php index 667033f..cadcd16 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php +++ b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php @@ -115,26 +115,32 @@ private function parseToml(string $toml): array private function arrayToToml(array $data): string { $toml = ''; + $sections = []; - // First output top-level keys + // First output top-level keys (non-arrays and inline arrays) foreach ($data as $key => $value) { if (!\is_array($value)) { $toml .= "{$key} = \"{$value}\"\n"; + } elseif ($this->isInlineArray($value)) { + $toml .= $this->formatKeyValue((string) $key, $value); + } else { + // Collect sections for later + $sections[$key] = $value; } } - if ($toml !== '') { + // Add separator between top-level values and sections + if ($toml !== '' && !empty($sections)) { $toml .= "\n"; } - // Then output sections - foreach ($data as $sectionKey => $sectionValue) { - if (\is_array($sectionValue)) { - $toml .= $this->sectionToToml($sectionKey, $sectionValue); - } + // Then output sections (associative arrays) + foreach ($sections as $sectionKey => $sectionValue) { + $toml .= $this->sectionToToml($sectionKey, $sectionValue); } - return $toml; + // Remove trailing whitespace + return \rtrim($toml); } /** @@ -148,42 +154,151 @@ private function sectionToToml(string $sectionName, array $data): string { $toml = ''; - // Check if this section has subsections + // Check if this section has subsections (associative arrays, not inline arrays) $hasSubsections = false; foreach ($data as $value) { - if (\is_array($value)) { + if (\is_array($value) && !$this->isInlineArray($value)) { $hasSubsections = true; break; } } if (!$hasSubsections) { - // Simple section with key-value pairs + // Simple section with key-value pairs (and possibly inline arrays) $toml .= "[{$sectionName}]\n"; foreach ($data as $key => $value) { - $toml .= "{$key} = \"{$value}\"\n"; + $toml .= $this->formatKeyValue((string) $key, $value); } $toml .= "\n"; } else { - // Section with subsections (like github.plugins) + // Handle mixed content: simple values and subsections + $simpleValues = []; + $subsections = []; + foreach ($data as $subKey => $subValue) { - if (\is_array($subValue)) { - $toml .= "[{$sectionName}.{$subKey}]\n"; - foreach ($subValue as $key => $value) { - $toml .= "{$key} = \"{$value}\"\n"; - } - $toml .= "\n"; + if (\is_array($subValue) && !$this->isInlineArray($subValue)) { + $subsections[$subKey] = $subValue; } else { - $toml .= "[{$sectionName}]\n"; - $toml .= "{$subKey} = \"{$subValue}\"\n"; - $toml .= "\n"; + $simpleValues[$subKey] = $subValue; + } + } + + // Only output the section header if there are simple values + // If there are only subsections, skip the intermediate section header + if (!empty($simpleValues)) { + $toml .= "[{$sectionName}]\n"; + foreach ($simpleValues as $key => $value) { + $toml .= $this->formatKeyValue((string) $key, $value); } + $toml .= "\n"; + } + + // Output subsections directly + foreach ($subsections as $subKey => $subValue) { + $toml .= $this->renderNestedSection("{$sectionName}.{$subKey}", $subValue); } } return $toml; } + /** + * Renders a nested section recursively. + * + * @param string $sectionPath Full section path (e.g., "github.plugins.logger") + * @param array $data Section data + * @return string TOML section content + */ + private function renderNestedSection(string $sectionPath, array $data): string + { + $toml = ''; + + // Separate simple values from subsections + $simpleValues = []; + $subsections = []; + + foreach ($data as $key => $value) { + if (\is_array($value) && !$this->isInlineArray($value)) { + $subsections[$key] = $value; + } else { + $simpleValues[$key] = $value; + } + } + + // Add section header if there are simple values OR if this section has no subsections + // (meaning it's an empty section that should be rendered) + if (!empty($simpleValues) || empty($subsections)) { + $toml .= "[{$sectionPath}]\n"; + foreach ($simpleValues as $key => $value) { + $toml .= $this->formatKeyValue((string) $key, $value); + } + $toml .= "\n"; + } + + // Render subsections + foreach ($subsections as $key => $value) { + $toml .= $this->renderNestedSection("{$sectionPath}.{$key}", $value); + } + + return $toml; + } + + /** + * Formats a key-value pair for TOML output. + * + * @param string $key The key + * @param mixed $value The value + * @return string Formatted TOML key-value pair + */ + private function formatKeyValue(string $key, mixed $value): string + { + if (\is_array($value)) { + // For array values, render them as inline arrays + return "{$key} = " . $this->formatArrayValue($value) . "\n"; + } + + return "{$key} = \"{$value}\"\n"; + } + + /** + * Formats an array value for TOML output. + * + * @param array $array The array to format + * @return string Formatted TOML array + */ + private function formatArrayValue(array $array): string + { + $items = []; + foreach ($array as $item) { + if (\is_array($item)) { + // Nested arrays are not typically supported in TOML inline arrays + // Convert to string representation + $items[] = '"' . \json_encode($item) . '"'; + } else { + $items[] = '"' . (string) $item . '"'; + } + } + + return '[' . \implode(', ', $items) . ']'; + } + + /** + * Determines if an array should be rendered as an inline array rather than a section. + * + * @param array $array The array to check + * @return bool True if it should be an inline array + */ + private function isInlineArray(array $array): bool + { + // If empty, treat as section (will render as empty section) + if (empty($array)) { + return false; + } + + // If all keys are numeric (sequential), it's an inline array + return \array_keys($array) === \range(0, \count($array) - 1); + } + private function deepMerge(array $array1, array $array2): array { $merged = $array1; diff --git a/src/Module/Velox/Internal/ConfigBuilder.php b/src/Module/Velox/Internal/ConfigBuilder.php index f99dfa0..530ff45 100644 --- a/src/Module/Velox/Internal/ConfigBuilder.php +++ b/src/Module/Velox/Internal/ConfigBuilder.php @@ -47,7 +47,7 @@ public function buildConfig(VeloxAction $action, Path $buildDir): Path $this->logger->debug('Building Velox configuration with pipeline...'); $pipeline = $this->pipelineBuilder->build(); - $context = new ConfigContext($action, buildDir: $buildDir); + $context = new ConfigContext($action, $buildDir); $result = $pipeline->process($context); $configContent = $result->tomlData->toToml(); diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php index 74f70e9..4070e9a 100644 --- a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php @@ -269,7 +269,6 @@ public function testToTomlConvertsDataToTomlString(): void [section] nested1 = "data1" nested2 = "data2" - \n TOML; // Act @@ -291,7 +290,7 @@ public function testToTomlHandlesNestedSections(): void ], ]; $tomlData = new TomlData($data); - $expectedToml = "[github.plugins]\nlogger = \"enabled\"\ncache = \"disabled\"\n\n"; + $expectedToml = "[github.plugins]\nlogger = \"enabled\"\ncache = \"disabled\""; // Act $result = $tomlData->toToml(); @@ -311,7 +310,11 @@ public function testToTomlHandlesMixedSectionTypes(): void ]; $tomlData = new TomlData($data); $expectedToml = << 'value'], $data); } + + public function testToTomlHandlesDeeplyNestedSections(): void + { + // Arrange + $data = [ + 'github' => [ + 'plugins' => [ + 'logger' => [ + 'ref' => 'v5.1.8', + 'owner' => 'roadrunner-server', + 'repository' => 'logger', + ], + 'server' => [ + 'ref' => 'v5.2.9', + 'owner' => 'roadrunner-server', + 'repository' => 'server', + ], + ], + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testToTomlHandlesInlineArrays(): void + { + // Arrange + $data = [ + 'features' => ['logging', 'caching', 'metrics'], + 'ports' => [8080, 9090, 3000], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testToTomlHandlesMixedTopLevelAndNestedStructures(): void + { + // Arrange + $data = [ + 'roadrunner' => [ + 'ref' => 'v2025.1.1', + ], + 'log' => [ + 'level' => 'debug', + 'mode' => 'dev', + ], + 'github' => [ + 'token' => [ + 'token' => '${GITHUB_TOKEN}', + ], + 'plugins' => [ + 'logger' => [ + 'ref' => 'v5.1.8', + 'owner' => 'roadrunner-server', + 'repository' => 'logger', + ], + ], + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testFromStringAndToTomlRoundTripWithNestedArrays(): void + { + // Arrange - This mimics the structure from velox.toml + $originalToml = <<toToml(); + $roundTripData = TomlData::fromString($convertedToml); + + // Assert + self::assertSame($tomlData->getData(), $roundTripData->getData()); + } + + public function testToTomlHandlesEmptyNestedSections(): void + { + // Arrange + $data = [ + 'section' => [ + 'empty_subsection' => [], + 'populated_subsection' => ['key' => 'value'], + ], + ]; + $tomlData = new TomlData($data); + $expectedToml = <<toToml(); + + // Assert + self::assertSame($expectedToml, $result); + } + + public function testParseTomlWithComplexNestedStructure(): void + { + // Arrange + $toml = << ['ref' => 'v2025.1.1'], + 'github' => [ + 'plugins' => [ + 'logger' => [ + 'ref' => 'v5.1.8', + 'owner' => 'roadrunner-server', + 'repository' => 'logger', + ], + 'temporal' => [ + 'ref' => 'v5.7.0', + 'owner' => 'temporalio', + 'repository' => 'roadrunner-temporal', + ], + ], + ], + ]; + + // Act + $tomlData = TomlData::fromString($toml); + + // Assert + self::assertSame($expectedData, $tomlData->getData()); + } } From 55f6034a496679e86d043b0baf7f8553384d3b4d Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 23:31:03 +0400 Subject: [PATCH 33/38] feat(velox): implement BuildRoadRunner API client and update configuration processing --- src/Bootstrap.php | 3 + src/Module/Velox/ApiClient.php | 18 +-- .../Velox/Internal/Client/BuildRoadRunner.php | 105 ++++++++++++++++++ .../Internal/Config/ConfigPipelineBuilder.php | 2 +- .../Processor/GitHubTokenProcessor.php | 2 +- 5 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 src/Module/Velox/Internal/Client/BuildRoadRunner.php diff --git a/src/Bootstrap.php b/src/Bootstrap.php index 053dad0..a85759c 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.php @@ -15,7 +15,9 @@ use Internal\DLoad\Module\HttpClient\Internal\NyholmFactoryImpl; use Internal\DLoad\Module\Repository\Internal\GitHub\Factory as GithubRepositoryFactory; use Internal\DLoad\Module\Repository\RepositoryProvider; +use Internal\DLoad\Module\Velox\ApiClient; use Internal\DLoad\Module\Velox\Builder; +use Internal\DLoad\Module\Velox\Internal\Client\BuildRoadRunner; use Internal\DLoad\Module\Velox\Internal\VeloxBuilder; use Internal\DLoad\Service\Container; @@ -109,6 +111,7 @@ public function withConfig( $this->container->bind(BinaryProvider::class, BinaryProviderImpl::class); $this->container->bind(Factory::class, NyholmFactoryImpl::class); $this->container->bind(Builder::class, VeloxBuilder::class); + $this->container->bind(ApiClient::class, BuildRoadRunner::class); return $this; } diff --git a/src/Module/Velox/ApiClient.php b/src/Module/Velox/ApiClient.php index 126d5eb..c434382 100644 --- a/src/Module/Velox/ApiClient.php +++ b/src/Module/Velox/ApiClient.php @@ -34,28 +34,12 @@ public function generateConfig( array $options = [], ): string; - /** - * Validates plugin specifications against the API. - * - * @param list $plugins Plugins to validate - * @return array Validation results - * @throws Exception\Api When API request fails - */ - public function validatePlugins(array $plugins): array; - /** * Retrieves available plugin information from the API. * - * @param string|null $search Optional search term + * @param non-empty-string|null $search Optional search term * @return array Available plugins * @throws Exception\Api When API request fails */ public function getAvailablePlugins(?string $search = null): array; - - /** - * Checks API availability and health. - * - * @return bool True if API is available - */ - public function isAvailable(): bool; } diff --git a/src/Module/Velox/Internal/Client/BuildRoadRunner.php b/src/Module/Velox/Internal/Client/BuildRoadRunner.php new file mode 100644 index 0000000..c14b23f --- /dev/null +++ b/src/Module/Velox/Internal/Client/BuildRoadRunner.php @@ -0,0 +1,105 @@ + \array_map(static fn(Plugin $plugin): string => $plugin->name, $plugins), + 'format' => 'toml', + ]; + + $golangVersion !== null and $requestData['golang_version'] = $golangVersion; + $binaryVersion !== null and $requestData['binary_version'] = $binaryVersion; + $options === [] or $requestData = \array_merge($requestData, $options); + + return $this->makeRequest( + 'POST', + '/plugins/generate-config', + $requestData, + ); + } + + public function getAvailablePlugins(?string $search = null): array + { + $query = []; + $search !== null and $query['search'] = $search; + + $response = $this->makeRequest('GET', '/plugins', query: $query); + + return \json_decode($response, true, 512, JSON_THROW_ON_ERROR); + } + + /** + * @param non-empty-string $method + * @param non-empty-string $endpoint + * @param array $data + * @param array $query + * @throws Api + */ + private function makeRequest( + string $method, + string $endpoint, + array $data = [], + array $query = [], + ): string { + try { + $uri = $this->httpFactory->uri(self::API_BASE_URL . $endpoint, $query); + $request = $this->httpFactory->request($method, $uri, [ + 'Accept' => 'text/plain', + 'Content-Type' => 'application/json', + ]); + + if ($data !== []) { + $body = $this->httpFactory->request('POST', '')->getBody(); + $body->write(\json_encode($data, JSON_THROW_ON_ERROR)); + $request = $request->withBody($body); + } + + $response = $this->httpFactory->client()->sendRequest($request); + + return $this->handleResponse($response); + } catch (\Throwable $e) { + throw new Api("API request failed: {$e->getMessage()}", 0, $e); + } + } + + /** + * @throws Api + */ + private function handleResponse(ResponseInterface $response): string + { + $statusCode = $response->getStatusCode(); + + if ($statusCode < 200 || $statusCode >= 300) { + throw new Api("API request failed with status {$statusCode}: {$response->getBody()->getContents()}"); + } + + return $response->getBody()->getContents(); + } +} diff --git a/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php b/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php index 8c80058..c720ad4 100644 --- a/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php +++ b/src/Module/Velox/Internal/Config/ConfigPipelineBuilder.php @@ -31,7 +31,7 @@ public function __construct( private readonly Container $container, private readonly array $pipes = [ BaseTemplateProcessor::class, - // RemoteApiProcessor::class, + RemoteApiProcessor::class, LocalFileProcessor::class, BuildMixinsProcessor::class, GitHubTokenProcessor::class, diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php index e731aa0..5117607 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/GitHubTokenProcessor.php @@ -30,7 +30,7 @@ public function __invoke(ConfigContext $context): ConfigContext return $context; } - $tomlData = $context->tomlData->set('github.token', $this->gitHub->token); + $tomlData = $context->tomlData->set('github.token.token', $this->gitHub->token); return $context ->withTomlData($tomlData) From b4ca4ad1fbc85a43cf2631a1a91738e1fdc9cd1c Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 27 Jul 2025 23:57:43 +0400 Subject: [PATCH 34/38] feat(velox): normalize RoadRunner ref and order TOML sections --- src/Module/Config/Schema/Action/Velox.php | 6 +- .../Internal/Config/Pipeline/TomlData.php | 57 ++++++- .../Internal/Config/Pipeline/TomlDataTest.php | 156 ++++++++++++++++++ 3 files changed, 214 insertions(+), 5 deletions(-) diff --git a/src/Module/Config/Schema/Action/Velox.php b/src/Module/Config/Schema/Action/Velox.php index 041c3bb..cd97c20 100644 --- a/src/Module/Config/Schema/Action/Velox.php +++ b/src/Module/Config/Schema/Action/Velox.php @@ -32,9 +32,9 @@ final class Velox #[XPath('@golang-version')] public ?string $golangVersion = null; - /** @var non-empty-string|null $binaryVersion RoadRunner version to display in --version */ - #[XPath('@binary-version')] - public ?string $binaryVersion = null; + /** @var non-empty-string|null $roadrunnerVersion RoadRunner version to display in --version */ + #[XPath('@roadrunner-version')] + public ?string $roadrunnerVersion = null; /** @var non-empty-string|null $configFile Path to local velox.toml file */ #[XPath('@config-file')] diff --git a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php index cadcd16..0f99fb0 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php +++ b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php @@ -56,7 +56,8 @@ public function set(string $path, mixed $value): self public function toToml(): string { - return $this->arrayToToml($this->data); + $normalizedData = $this->normalizeData($this->data); + return $this->arrayToToml($normalizedData); } public function getData(): array @@ -134,8 +135,11 @@ private function arrayToToml(array $data): string $toml .= "\n"; } + // Order sections with roadrunner first, then debug, logs, github, gitlab, etc. + $orderedSections = $this->orderSections($sections); + // Then output sections (associative arrays) - foreach ($sections as $sectionKey => $sectionValue) { + foreach ($orderedSections as $sectionKey => $sectionValue) { $toml .= $this->sectionToToml($sectionKey, $sectionValue); } @@ -143,6 +147,55 @@ private function arrayToToml(array $data): string return \rtrim($toml); } + /** + * Orders sections with priority: roadrunner first, then debug, logs, github, gitlab, etc. + * + * @param array $sections Sections to order + * @return array Ordered sections + */ + private function orderSections(array $sections): array + { + $priority = ['roadrunner', 'debug', 'log', 'github', 'gitlab']; + $orderedSections = []; + $remainingSections = $sections; + + // First add priority sections in order + foreach ($priority as $sectionKey) { + if (isset($remainingSections[$sectionKey])) { + $orderedSections[$sectionKey] = $remainingSections[$sectionKey]; + unset($remainingSections[$sectionKey]); + } + } + + // Then add any remaining sections + foreach ($remainingSections as $sectionKey => $sectionValue) { + $orderedSections[$sectionKey] = $sectionValue; + } + + return $orderedSections; + } + + /** + * Normalizes configuration data before TOML conversion. + * + * @param array $data Configuration data + * @return array Normalized data + */ + private function normalizeData(array $data): array + { + $normalized = $data; + + // Normalize roadrunner.ref directive - add "v" prefix if not exists + if (isset($normalized['roadrunner']['ref'])) { + $ref = $normalized['roadrunner']['ref']; + if (\is_string($ref) && $ref !== '' && !\str_starts_with($ref, 'v')) { + $normalized['roadrunner']['ref'] = 'v' . $ref; + } + } + + return $normalized; + } + /** * Converts a section to TOML format. * diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php index 4070e9a..f4e1284 100644 --- a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php @@ -624,4 +624,160 @@ public function testParseTomlWithComplexNestedStructure(): void // Assert self::assertSame($expectedData, $tomlData->getData()); } + + public function testToTomlOrdersSectionsWithRoadrunnerFirst(): void + { + // Arrange - sections in different order + $data = [ + 'github' => ['plugins' => ['logger' => 'enabled']], + 'log' => ['level' => 'debug'], + 'roadrunner' => ['ref' => 'v2025.1.1'], + 'debug' => ['enabled' => 'true'], + 'other' => ['key' => 'value'], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - roadrunner should be first, then debug, log, github, then others + $expectedToml = << ['ref' => '2025.1.1'], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - "v" prefix should be added + $expectedToml = << ['ref' => 'v2025.1.1'], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - "v" prefix should be preserved + $expectedToml = << ['ref' => ''], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - empty ref should remain empty + $expectedToml = << ['ref' => 123], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - non-string ref should remain unchanged + $expectedToml = << ['key' => 'value'], + 'gitlab' => ['url' => 'gitlab.com'], + 'github' => ['token' => 'secret'], + 'log' => ['level' => 'info'], + 'debug' => ['enabled' => 'false'], + 'roadrunner' => ['ref' => '2025.1.1'], + ]; + $tomlData = new TomlData($data); + + // Act + $result = $tomlData->toToml(); + + // Assert - proper ordering and normalization + $expectedToml = << Date: Mon, 28 Jul 2025 00:06:04 +0400 Subject: [PATCH 35/38] feat(velox): replace binary-version with roadrunner-ref in configuration and documentation --- README-es.md | 8 +++---- README-ru.md | 8 +++---- README-zh.md | 8 +++---- README.md | 8 +++---- dload.xsd | 4 ++-- src/Module/Config/Schema/Action/Velox.php | 7 ++++-- src/Module/Velox/ApiClient.php | 2 +- .../Velox/Internal/Client/BuildRoadRunner.php | 2 +- .../Processor/BuildMixinsProcessor.php | 6 ++--- .../Pipeline/Processor/RemoteApiProcessor.php | 2 +- .../Internal/Config/Pipeline/TomlData.php | 24 +------------------ 11 files changed, 30 insertions(+), 49 deletions(-) diff --git a/README-es.md b/README-es.md index 6ed7828..38d5186 100644 --- a/README-es.md +++ b/README-es.md @@ -338,7 +338,7 @@ DLoad soporta la construcción de binarios personalizados de RoadRunner usando l ``` @@ -349,7 +349,7 @@ DLoad soporta la construcción de binarios personalizados de RoadRunner usando l |-----------|-------------|-------------------| | `velox-version` | Versión de la herramienta Velox | Última | | `golang-version` | Versión requerida de Go | Última | -| `binary-version` | Versión de RoadRunner para mostrar en `rr --version` | Última | +| `roadrunner-ref` | Referencia Git de RoadRunner (tag, commit o rama) para la compilación | Última | | `config-file` | Ruta al archivo velox.toml local | `./velox.toml` | | `binary-path` | Ruta donde guardar el binario construido de RoadRunner | `./rr` | @@ -384,7 +384,7 @@ Puedes descargar Velox como parte de tu proceso de construcción en lugar de dep + roadrunner-ref="2024.1.5" /> ``` @@ -400,7 +400,7 @@ Esto asegura versiones consistentes de Velox entre diferentes entornos y miembro diff --git a/README-ru.md b/README-ru.md index e3919f8..190bee9 100644 --- a/README-ru.md +++ b/README-ru.md @@ -339,7 +339,7 @@ DLoad поддерживает сборку кастомных бинарник ``` @@ -350,7 +350,7 @@ DLoad поддерживает сборку кастомных бинарник |---------|----------|--------------| | `velox-version` | Версия инструмента сборки Velox | Последняя | | `golang-version` | Требуемая версия Go | Последняя | -| `binary-version` | Версия RoadRunner для отображения в `rr --version` | Последняя | +| `roadrunner-ref` | Git-ссылка RoadRunner (тег, коммит или ветка) для сборки | Последняя | | `config-file` | Путь к локальному файлу velox.toml | `./velox.toml` | | `binary-path` | Путь для сохранения собранного бинарника RoadRunner | `./rr` | @@ -385,7 +385,7 @@ DLoad автоматически управляет процессом сбор + roadrunner-ref="2024.1.5" /> ``` @@ -401,7 +401,7 @@ DLoad автоматически управляет процессом сбор diff --git a/README-zh.md b/README-zh.md index 60a54e4..7f2ddc4 100644 --- a/README-zh.md +++ b/README-zh.md @@ -338,7 +338,7 @@ DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文 ``` @@ -349,7 +349,7 @@ DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文 |-----------|-------------|---------| | `velox-version` | Velox 构建工具版本 | 最新版 | | `golang-version` | 所需的 Go 版本 | 最新版 | -| `binary-version` | 在 `rr --version` 中显示的 RoadRunner 版本 | 最新版 | +| `roadrunner-ref` | 用于构建的 RoadRunner Git 引用(标签、提交或分支) | 最新版 | | `config-file` | 本地 velox.toml 文件路径 | `./velox.toml` | | `binary-path` | 保存构建的 RoadRunner 二进制文件的路径 | `./rr` | @@ -384,7 +384,7 @@ DLoad 会自动处理构建过程: + roadrunner-ref="2024.1.5" /> ``` @@ -400,7 +400,7 @@ DLoad 会自动处理构建过程: diff --git a/README.md b/README.md index 6e38d56..2a12396 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ DLoad supports building custom RoadRunner binaries using the Velox build tool. T ``` @@ -349,7 +349,7 @@ DLoad supports building custom RoadRunner binaries using the Velox build tool. T |-----------|-------------|---------| | `velox-version` | Version of Velox build tool | Latest | | `golang-version` | Required Go version | Latest | -| `binary-version` | RoadRunner version to display in `rr --version` | Latest | +| `roadrunner-ref` | RoadRunner Git reference (tag, commit, or branch) to use for building | Latest | | `config-file` | Path to local velox.toml file | `./velox.toml` | | `binary-path` | Path to save the built RoadRunner binary | `./rr` | @@ -384,7 +384,7 @@ You can download Velox as part of your build process instead of relying on a glo + roadrunner-ref="2024.1.5" /> ``` @@ -400,7 +400,7 @@ This ensures consistent Velox versions across different environments and team me diff --git a/dload.xsd b/dload.xsd index fec2cc7..b6a6c2d 100644 --- a/dload.xsd +++ b/dload.xsd @@ -108,9 +108,9 @@ Required Go version constraint - + - RoadRunner version to display in --version + RoadRunner Git reference (tag, commit, or branch) to use for building diff --git a/src/Module/Config/Schema/Action/Velox.php b/src/Module/Config/Schema/Action/Velox.php index cd97c20..ea0095c 100644 --- a/src/Module/Config/Schema/Action/Velox.php +++ b/src/Module/Config/Schema/Action/Velox.php @@ -32,8 +32,11 @@ final class Velox #[XPath('@golang-version')] public ?string $golangVersion = null; - /** @var non-empty-string|null $roadrunnerVersion RoadRunner version to display in --version */ - #[XPath('@roadrunner-version')] + /** + * @var non-empty-string|null $roadrunnerVersion RoadRunner Git reference (tag, commit, or branch) + * to use for building. + */ + #[XPath('@roadrunner-ref')] public ?string $roadrunnerVersion = null; /** @var non-empty-string|null $configFile Path to local velox.toml file */ diff --git a/src/Module/Velox/ApiClient.php b/src/Module/Velox/ApiClient.php index c434382..b07388f 100644 --- a/src/Module/Velox/ApiClient.php +++ b/src/Module/Velox/ApiClient.php @@ -21,7 +21,7 @@ interface ApiClient * * @param list $plugins List of plugins to include * @param string|null $golangVersion Go version constraint - * @param string|null $binaryVersion RoadRunner binary version + * @param string|null $binaryVersion RoadRunner version (reference) * @param array $options Additional configuration options * @return string Generated velox.toml content * @throws Exception\Api When API request fails diff --git a/src/Module/Velox/Internal/Client/BuildRoadRunner.php b/src/Module/Velox/Internal/Client/BuildRoadRunner.php index c14b23f..e43d9b1 100644 --- a/src/Module/Velox/Internal/Client/BuildRoadRunner.php +++ b/src/Module/Velox/Internal/Client/BuildRoadRunner.php @@ -35,7 +35,7 @@ public function generateConfig( ]; $golangVersion !== null and $requestData['golang_version'] = $golangVersion; - $binaryVersion !== null and $requestData['binary_version'] = $binaryVersion; + $binaryVersion !== null and $requestData['roadrunner_ref'] = $binaryVersion; $options === [] or $requestData = \array_merge($requestData, $options); return $this->makeRequest( diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php index 16693c8..2259ded 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php @@ -23,9 +23,9 @@ public function __invoke(ConfigContext $context): ConfigContext $tomlData = $context->tomlData; $appliedMixins = []; - if ($context->action->binaryVersion !== null) { - $tomlData = $tomlData->set('roadrunner.ref', $context->action->binaryVersion); - $appliedMixins[] = 'binary_version'; + if ($context->action->roadrunnerVersion !== null) { + $tomlData = $tomlData->set('roadrunner.ref', $context->action->roadrunnerVersion); + $appliedMixins[] = 'roadrunner_ref'; } if ($appliedMixins === []) { diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php index bbeba96..610923a 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/RemoteApiProcessor.php @@ -34,7 +34,7 @@ public function __invoke(ConfigContext $context): ConfigContext $apiToml = $this->apiClient->generateConfig( $context->action->plugins, $context->action->golangVersion, - $context->action->binaryVersion, + $context->action->roadrunnerVersion, ); $apiData = TomlData::fromString($apiToml); diff --git a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php index 0f99fb0..7292293 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php +++ b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php @@ -56,8 +56,7 @@ public function set(string $path, mixed $value): self public function toToml(): string { - $normalizedData = $this->normalizeData($this->data); - return $this->arrayToToml($normalizedData); + return $this->arrayToToml($this->data); } public function getData(): array @@ -175,27 +174,6 @@ private function orderSections(array $sections): array return $orderedSections; } - /** - * Normalizes configuration data before TOML conversion. - * - * @param array $data Configuration data - * @return array Normalized data - */ - private function normalizeData(array $data): array - { - $normalized = $data; - - // Normalize roadrunner.ref directive - add "v" prefix if not exists - if (isset($normalized['roadrunner']['ref'])) { - $ref = $normalized['roadrunner']['ref']; - if (\is_string($ref) && $ref !== '' && !\str_starts_with($ref, 'v')) { - $normalized['roadrunner']['ref'] = 'v' . $ref; - } - } - - return $normalized; - } - /** * Converts a section to TOML format. * From 760dcc98acb07580715348c7ebc6509e8d6e51bb Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 28 Jul 2025 00:17:44 +0400 Subject: [PATCH 36/38] docs(velox): update action attribute descriptions in README files for clarity and accuracy --- README-es.md | 14 +++++++------- README-ru.md | 14 +++++++------- README-zh.md | 14 +++++++------- README.md | 14 +++++++------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README-es.md b/README-es.md index 38d5186..08b5849 100644 --- a/README-es.md +++ b/README-es.md @@ -345,13 +345,13 @@ DLoad soporta la construcción de binarios personalizados de RoadRunner usando l ### Atributos de Acción Velox -| Atributo | Descripción | Valor por defecto | -|-----------|-------------|-------------------| -| `velox-version` | Versión de la herramienta Velox | Última | -| `golang-version` | Versión requerida de Go | Última | -| `roadrunner-ref` | Referencia Git de RoadRunner (tag, commit o rama) para la compilación | Última | -| `config-file` | Ruta al archivo velox.toml local | `./velox.toml` | -| `binary-path` | Ruta donde guardar el binario construido de RoadRunner | `./rr` | +| Atributo | Descripción | +|-----------|-------------| +| `velox-version` | Restricción de versión para la herramienta de construcción Velox a utilizar | +| `golang-version` | Restricción de versión de Go requerida para construir RoadRunner | +| `roadrunner-ref` | Referencia Git de RoadRunner (tag, commit o rama) a usar como base para la construcción | +| `config-file` | Ruta al archivo de configuración base que puede fusionarse con respuestas de API remotas u otras fuentes | +| `binary-path` | Ruta de salida para el binario RoadRunner construido. La extensión del archivo se agrega automáticamente según el SO (`.exe` para Windows). Por defecto usa el directorio de trabajo actual | ### Proceso de Construcción diff --git a/README-ru.md b/README-ru.md index 190bee9..236edd6 100644 --- a/README-ru.md +++ b/README-ru.md @@ -346,13 +346,13 @@ DLoad поддерживает сборку кастомных бинарник ### Атрибуты Velox-действия -| Атрибут | Описание | По умолчанию | -|---------|----------|--------------| -| `velox-version` | Версия инструмента сборки Velox | Последняя | -| `golang-version` | Требуемая версия Go | Последняя | -| `roadrunner-ref` | Git-ссылка RoadRunner (тег, коммит или ветка) для сборки | Последняя | -| `config-file` | Путь к локальному файлу velox.toml | `./velox.toml` | -| `binary-path` | Путь для сохранения собранного бинарника RoadRunner | `./rr` | +| Атрибут | Описание | +|---------|----------| +| `velox-version` | Ограничение версии для используемого инструмента сборки Velox | +| `golang-version` | Ограничение версии Go, необходимой для сборки RoadRunner | +| `roadrunner-ref` | Git-ссылка RoadRunner (тег, коммит или ветка), используемая как основа для сборки | +| `config-file` | Путь к базовому конфигурационному файлу, который может объединяться с ответами удаленного API или другими источниками | +| `binary-path` | Путь вывода для собранного бинарника RoadRunner. Расширение файла автоматически добавляется в зависимости от ОС (`.exe` для Windows). По умолчанию используется текущий рабочий каталог | ### Процесс сборки diff --git a/README-zh.md b/README-zh.md index 7f2ddc4..10556f2 100644 --- a/README-zh.md +++ b/README-zh.md @@ -345,13 +345,13 @@ DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文 ### Velox 动作属性 -| 属性 | 说明 | 默认值 | -|-----------|-------------|---------| -| `velox-version` | Velox 构建工具版本 | 最新版 | -| `golang-version` | 所需的 Go 版本 | 最新版 | -| `roadrunner-ref` | 用于构建的 RoadRunner Git 引用(标签、提交或分支) | 最新版 | -| `config-file` | 本地 velox.toml 文件路径 | `./velox.toml` | -| `binary-path` | 保存构建的 RoadRunner 二进制文件的路径 | `./rr` | +| 属性 | 说明 | +|-----------|-------------| +| `velox-version` | 使用的 Velox 构建工具的版本约束 | +| `golang-version` | 构建 RoadRunner 所需的 Go 版本约束 | +| `roadrunner-ref` | 用作构建基础的 RoadRunner Git 引用(标签、提交或分支) | +| `config-file` | 基础配置文件路径,可能与远程 API 响应或其他源合并 | +| `binary-path` | 构建的 RoadRunner 二进制文件输出路径。文件扩展名根据操作系统自动添加(Windows 下为 `.exe`)。默认为当前工作目录 | ### 构建流程 diff --git a/README.md b/README.md index 2a12396..c23e523 100644 --- a/README.md +++ b/README.md @@ -345,13 +345,13 @@ DLoad supports building custom RoadRunner binaries using the Velox build tool. T ### Velox Action Attributes -| Attribute | Description | Default | -|-----------|-------------|---------| -| `velox-version` | Version of Velox build tool | Latest | -| `golang-version` | Required Go version | Latest | -| `roadrunner-ref` | RoadRunner Git reference (tag, commit, or branch) to use for building | Latest | -| `config-file` | Path to local velox.toml file | `./velox.toml` | -| `binary-path` | Path to save the built RoadRunner binary | `./rr` | +| Attribute | Description | +|-----------|-------------| +| `velox-version` | Version constraint for the Velox build tool to use | +| `golang-version` | Go version constraint required for building RoadRunner | +| `roadrunner-ref` | RoadRunner Git reference (tag, commit, or branch) to use as the base for building | +| `config-file` | Path to base configuration file that may be merged with remote API responses or other sources | +| `binary-path` | Output path for the built RoadRunner binary. File extension is automatically added based on OS (`.exe` for Windows). Defaults to current working directory | ### Build Process From 5a0494fdfaad805c9897a90df6dc58a81c293dd4 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 28 Jul 2025 10:22:54 +0400 Subject: [PATCH 37/38] feat(velox): add debug flag to configuration examples and update version references in README files --- README-es.md | 12 +++++----- README-ru.md | 6 +++-- README-zh.md | 10 +++++---- README.md | 22 ++++++++++++------- dload.xsd | 5 +++++ src/Module/Config/Schema/Action/Velox.php | 4 ++++ .../Processor/BaseTemplateProcessor.php | 2 +- .../Processor/BuildMixinsProcessor.php | 8 +++---- 8 files changed, 44 insertions(+), 25 deletions(-) diff --git a/README-es.md b/README-es.md index 08b5849..bae131a 100644 --- a/README-es.md +++ b/README-es.md @@ -335,11 +335,12 @@ DLoad soporta la construcción de binarios personalizados de RoadRunner usando l - + ``` @@ -352,6 +353,7 @@ DLoad soporta la construcción de binarios personalizados de RoadRunner usando l | `roadrunner-ref` | Referencia Git de RoadRunner (tag, commit o rama) a usar como base para la construcción | | `config-file` | Ruta al archivo de configuración base que puede fusionarse con respuestas de API remotas u otras fuentes | | `binary-path` | Ruta de salida para el binario RoadRunner construido. La extensión del archivo se agrega automáticamente según el SO (`.exe` para Windows). Por defecto usa el directorio de trabajo actual | +| `debug` | Construir RoadRunner con símbolos de depuración para perfilarlo con pprof (booleano, por defecto `false`) | ### Proceso de Construcción diff --git a/README-ru.md b/README-ru.md index 236edd6..85f4f9a 100644 --- a/README-ru.md +++ b/README-ru.md @@ -337,10 +337,11 @@ DLoad поддерживает сборку кастомных бинарник + binary-path="./bin/rr" + debug="true" /> ``` @@ -353,6 +354,7 @@ DLoad поддерживает сборку кастомных бинарник | `roadrunner-ref` | Git-ссылка RoadRunner (тег, коммит или ветка), используемая как основа для сборки | | `config-file` | Путь к базовому конфигурационному файлу, который может объединяться с ответами удаленного API или другими источниками | | `binary-path` | Путь вывода для собранного бинарника RoadRunner. Расширение файла автоматически добавляется в зависимости от ОС (`.exe` для Windows). По умолчанию используется текущий рабочий каталог | +| `debug` | Собрать RoadRunner с отладочными символами для профилирования с помощью pprof (логическое значение, по умолчанию `false`) | ### Процесс сборки diff --git a/README-zh.md b/README-zh.md index 10556f2..668279b 100644 --- a/README-zh.md +++ b/README-zh.md @@ -336,10 +336,11 @@ DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文 + velox-version="2025.1.1" + golang-version="^1.22" + roadrunner-ref="2024.1.5" + binary-path="./bin/rr" + debug="true" /> ``` @@ -352,6 +353,7 @@ DLoad 支持使用 Velox 构建工具来构建自定义 RoadRunner 二进制文 | `roadrunner-ref` | 用作构建基础的 RoadRunner Git 引用(标签、提交或分支) | | `config-file` | 基础配置文件路径,可能与远程 API 响应或其他源合并 | | `binary-path` | 构建的 RoadRunner 二进制文件输出路径。文件扩展名根据操作系统自动添加(Windows 下为 `.exe`)。默认为当前工作目录 | +| `debug` | 使用调试符号构建 RoadRunner,以便使用 pprof 进行性能分析(布尔值,默认为 `false`) | ### 构建流程 diff --git a/README.md b/README.md index c23e523..f3ae368 100644 --- a/README.md +++ b/README.md @@ -333,13 +333,20 @@ DLoad supports building custom RoadRunner binaries using the Velox build tool. T - + - + + + + + + + ``` @@ -352,6 +359,7 @@ DLoad supports building custom RoadRunner binaries using the Velox build tool. T | `roadrunner-ref` | RoadRunner Git reference (tag, commit, or branch) to use as the base for building | | `config-file` | Path to base configuration file that may be merged with remote API responses or other sources | | `binary-path` | Output path for the built RoadRunner binary. File extension is automatically added based on OS (`.exe` for Windows). Defaults to current working directory | +| `debug` | Build RoadRunner with debug symbols to profile it with pprof (boolean, defaults to `false`) | ### Build Process @@ -416,8 +424,6 @@ This ensures consistent Velox versions across different environments and team me ./vendor/bin/dload build --config=custom-rr.xml ``` -The built RoadRunner binary will include only the plugins specified in your `velox.toml` file, reducing binary size and improving performance for your specific use case. - ## Custom Software Registry ### Defining Software diff --git a/dload.xsd b/dload.xsd index b6a6c2d..a73eac3 100644 --- a/dload.xsd +++ b/dload.xsd @@ -123,6 +123,11 @@ Path to the RoadRunner binary to build + + + Build RoadRunner with debug symbols to profile it with pprof + + diff --git a/src/Module/Config/Schema/Action/Velox.php b/src/Module/Config/Schema/Action/Velox.php index ea0095c..f8c8929 100644 --- a/src/Module/Config/Schema/Action/Velox.php +++ b/src/Module/Config/Schema/Action/Velox.php @@ -50,4 +50,8 @@ final class Velox /** @var non-empty-string|null $binaryPath Path to the RoadRunner binary to build */ #[XPath('@binary-path')] public ?string $binaryPath = null; + + /** @var bool $debug Build RoadRunner with debug symbols to profile it with pprof */ + #[XPath('@debug')] + public bool $debug = false; } diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/BaseTemplateProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/BaseTemplateProcessor.php index a5f6bc6..9e21ea8 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/BaseTemplateProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/BaseTemplateProcessor.php @@ -28,7 +28,7 @@ public function __invoke(ConfigContext $context): ConfigContext 'mode' => 'dev', ], 'debug' => [ - 'enabled ' => true, + 'enabled ' => false, ], ]); diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php index 2259ded..020737a 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/BuildMixinsProcessor.php @@ -10,8 +10,7 @@ /** * Build mixins processor * - * Applies build action settings such as binary version and Go version. - * Runs when version settings are provided in the action. + * Applies build action settings such as RoadRunner version and debug flags. * * @internal * @psalm-internal Internal\DLoad\Module\Velox @@ -28,9 +27,8 @@ public function __invoke(ConfigContext $context): ConfigContext $appliedMixins[] = 'roadrunner_ref'; } - if ($appliedMixins === []) { - return $context; - } + $tomlData = $tomlData->set('debug.enabled', $context->action->debug); + $appliedMixins[] = 'debug_enabled'; return $context->withTomlData($tomlData) ->addMetadata('build_mixins_applied', true) From 5011ec7652c877145fcd55d77fb5780dd4046b3a Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Mon, 28 Jul 2025 10:50:54 +0400 Subject: [PATCH 38/38] chore(velox): update psalm baseline and enhance configuration classes with detailed docblocks --- psalm-baseline.xml | 45 +++- .../Config/Pipeline/ConfigContext.php | 39 ++++ .../Pipeline/Processor/LocalFileProcessor.php | 2 +- .../Internal/Config/Pipeline/TomlData.php | 214 ++++++++++++++---- src/Module/Velox/Internal/ConfigBuilder.php | 2 +- .../Internal/Config/Pipeline/TomlDataTest.php | 22 +- 6 files changed, 255 insertions(+), 69 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 3b4f7fd..0f1aded 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -81,12 +81,6 @@ - - chmod]]> - - - chmod]]> - binary]]> @@ -393,6 +387,45 @@ isSatisfiedBy($this->version)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php index c3d3fae..2558e80 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php +++ b/src/Module/Velox/Internal/Config/Pipeline/ConfigContext.php @@ -18,6 +18,14 @@ */ final class ConfigContext { + /** + * Creates a new immutable configuration context. + * + * @param VeloxAction $action The Velox action configuration containing build settings + * @param Path $buildDir The directory where the build process will take place + * @param TomlData $tomlData The TOML configuration data being processed through the pipeline + * @param array $metadata Additional metadata collected during pipeline processing + */ public function __construct( public readonly VeloxAction $action, public readonly Path $buildDir, @@ -25,19 +33,50 @@ public function __construct( public readonly array $metadata = [], ) {} + /** + * Creates a new context with updated TOML data. + * + * This method returns a new immutable instance with the specified TOML data, + * preserving all other context properties (action, build directory, and metadata). + * + * @param TomlData $tomlData The new TOML configuration data + * @return self A new context instance with the updated TOML data + */ public function withTomlData(TomlData $tomlData): self { return new self($this->action, $this->buildDir, $tomlData, $this->metadata); } + /** + * Creates a new context with completely replaced metadata. + * + * This method returns a new immutable instance with the specified metadata array, + * completely replacing any existing metadata. To add individual entries while + * preserving existing metadata, use addMetadata() instead. + * + * @param array $metadata The complete metadata array to set + * @return self A new context instance with the replaced metadata + */ public function withMetadata(array $metadata): self { return new self($this->action, $this->buildDir, $this->tomlData, $metadata); } + /** + * Adds a single metadata entry to the context. + * + * This method returns a new immutable instance with the specified key-value pair + * added to the metadata, preserving all existing metadata entries. If the key + * already exists, its value will be overwritten. + * + * @param non-empty-string $key The metadata key to add or update + * @param mixed $value The metadata value to associate with the key + * @return self A new context instance with the updated metadata + */ public function addMetadata(string $key, mixed $value): self { $metadata = $this->metadata; + /** @var mixed */ $metadata[$key] = $value; return $this->withMetadata($metadata); } diff --git a/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php b/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php index a89912e..e23fb5a 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php +++ b/src/Module/Velox/Internal/Config/Pipeline/Processor/LocalFileProcessor.php @@ -36,7 +36,7 @@ public function __invoke(ConfigContext $context): ConfigContext ); } - $localToml = \file_get_contents($configPath->__toString()); + $localToml = @\file_get_contents($configPath->__toString()); $localToml === false and throw new ConfigException( "Failed to read local config file: {$configPath}.", diff --git a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php index 7292293..d0a68a0 100644 --- a/src/Module/Velox/Internal/Config/Pipeline/TomlData.php +++ b/src/Module/Velox/Internal/Config/Pipeline/TomlData.php @@ -16,10 +16,24 @@ */ final class TomlData { + /** + * Creates a new immutable TOML data container. + * + * @param array $data The configuration data array + */ public function __construct( private readonly array $data = [], ) {} + /** + * Creates a new TOML data instance from a TOML string. + * + * Parses the provided TOML content and returns a new immutable instance + * containing the parsed configuration data. + * + * @param string $toml The TOML content to parse + * @return self A new TomlData instance with the parsed data + */ public static function fromString(string $toml): self { $instance = new self(); @@ -41,12 +55,33 @@ public static function mergeTomlStrings(string $localToml, string $remoteToml): return $local->merge($remote)->toToml(); } + /** + * Merges another TOML data instance into this one. + * + * Performs a deep merge where arrays from the other instance are recursively + * merged with arrays in this instance. Non-array values from the other instance + * will overwrite values in this instance. + * + * @param TomlData $other The TOML data to merge into this instance + * @return self A new TomlData instance with the merged data + */ public function merge(TomlData $other): self { $merged = $this->deepMerge($this->data, $other->data); return new self($merged); } + /** + * Sets a nested value using dot notation path. + * + * Creates a new immutable instance with the specified value set at the given path. + * The path uses dot notation to access nested array keys (e.g., "debug.enabled"). + * If intermediate keys don't exist, they will be created as arrays. + * + * @param string $path The dot-notation path to the value (e.g., "roadrunner.ref") + * @param mixed $value The value to set at the specified path + * @return self A new TomlData instance with the updated value + */ public function set(string $path, mixed $value): self { $data = $this->data; @@ -54,11 +89,27 @@ public function set(string $path, mixed $value): self return new self($data); } + /** + * Converts the internal data array to TOML string format. + * + * This is the main public API for serializing TOML data back to string format. + * Delegates to the internal arrayToToml method for the actual conversion logic. + * + * @return string The TOML-formatted string representation of the data + */ public function toToml(): string { return $this->arrayToToml($this->data); } + /** + * Returns the raw configuration data array. + * + * Provides direct access to the internal data structure for cases where + * array manipulation is needed instead of TOML string operations. + * + * @return array The internal data array + */ public function getData(): array { return $this->data; @@ -73,31 +124,37 @@ public function getData(): array private function parseToml(string $toml): array { $result = []; - $currentSection = null; + $currentSection = null; // Track the current section context (e.g., "roadrunner" or "github.plugins") $lines = \explode("\n", $toml); foreach ($lines as $line) { $line = \trim($line); - // Skip empty lines and comments + // Skip empty lines and comments (lines starting with #) if ($line === '' || \str_starts_with($line, '#')) { continue; } - // Section headers like [roadrunner] or [github.plugins.logger] + // Parse section headers like [roadrunner] or [github.plugins.logger] + // These define the namespace for subsequent key-value pairs if (\preg_match('/^\[([^\]]+)\]$/', $line, $matches)) { - $currentSection = $matches[1]; + $currentSection = $matches[1]; // Store the full section path continue; } - // Key-value pairs + // Parse key-value pairs in the format: key = value + // Handles both quoted and unquoted values if (\preg_match('/^([^=]+)=(.+)$/', $line, $matches)) { $key = \trim($matches[1]); + // Remove surrounding quotes and whitespace from values $value = \trim($matches[2], ' "\''); if ($currentSection === null) { + // Top-level key-value pair (no section) $result[$key] = $value; } else { + // Nested key-value pair within a section + // Combine section path with key using dot notation $this->setNestedValue($result, $currentSection . '.' . $key, $value); } } @@ -115,34 +172,40 @@ private function parseToml(string $toml): array private function arrayToToml(array $data): string { $toml = ''; - $sections = []; + $sections = []; // Collect associative arrays that will become TOML sections - // First output top-level keys (non-arrays and inline arrays) + // First pass: output top-level scalar values and inline arrays + // This ensures proper TOML structure with values before sections foreach ($data as $key => $value) { if (!\is_array($value)) { + // Simple scalar value - output directly as key = "value" $toml .= "{$key} = \"{$value}\"\n"; } elseif ($this->isInlineArray($value)) { + // Sequential array - format as inline TOML array [item1, item2, ...] $toml .= $this->formatKeyValue((string) $key, $value); } else { - // Collect sections for later + // Associative array - defer to sections for proper TOML structure $sections[$key] = $value; } } - // Add separator between top-level values and sections + // Add visual separator between top-level values and sections + // This improves readability of the generated TOML if ($toml !== '' && !empty($sections)) { $toml .= "\n"; } - // Order sections with roadrunner first, then debug, logs, github, gitlab, etc. + // Apply custom ordering to sections for consistent output + // Prioritizes commonly used sections like 'roadrunner' first $orderedSections = $this->orderSections($sections); - // Then output sections (associative arrays) + // Second pass: output all sections (associative arrays) + // Each section becomes a [section.name] block in TOML foreach ($orderedSections as $sectionKey => $sectionValue) { $toml .= $this->sectionToToml($sectionKey, $sectionValue); } - // Remove trailing whitespace + // Clean up any trailing whitespace for cleaner output return \rtrim($toml); } @@ -154,19 +217,23 @@ private function arrayToToml(array $data): string */ private function orderSections(array $sections): array { + // Define priority order for commonly used configuration sections + // This ensures consistent output format with important sections first $priority = ['roadrunner', 'debug', 'log', 'github', 'gitlab']; $orderedSections = []; - $remainingSections = $sections; + $remainingSections = $sections; // Copy to track unprocessed sections - // First add priority sections in order + // First pass: add priority sections in their defined order + // This maintains a predictable structure for configuration files foreach ($priority as $sectionKey) { if (isset($remainingSections[$sectionKey])) { $orderedSections[$sectionKey] = $remainingSections[$sectionKey]; - unset($remainingSections[$sectionKey]); + unset($remainingSections[$sectionKey]); // Remove from remaining list } } - // Then add any remaining sections + // Second pass: append any remaining sections that weren't in priority list + // These will appear after priority sections in their original order foreach ($remainingSections as $sectionKey => $sectionValue) { $orderedSections[$sectionKey] = $sectionValue; } @@ -185,37 +252,42 @@ private function sectionToToml(string $sectionName, array $data): string { $toml = ''; - // Check if this section has subsections (associative arrays, not inline arrays) + // Analyze section structure to determine rendering approach + // Check if this section contains nested associative arrays (subsections) $hasSubsections = false; foreach ($data as $value) { if (\is_array($value) && !$this->isInlineArray($value)) { $hasSubsections = true; - break; + break; // Early exit once we find a subsection } } if (!$hasSubsections) { - // Simple section with key-value pairs (and possibly inline arrays) + // Simple flat section: only scalar values and inline arrays + // Format: [section_name] followed by key=value pairs $toml .= "[{$sectionName}]\n"; foreach ($data as $key => $value) { $toml .= $this->formatKeyValue((string) $key, $value); } - $toml .= "\n"; + $toml .= "\n"; // Add blank line after section } else { - // Handle mixed content: simple values and subsections - $simpleValues = []; - $subsections = []; + // Complex section with mixed content: both simple values and nested sections + // Separate simple values from nested subsections for proper TOML structure + $simpleValues = []; // Scalar values and inline arrays + $subsections = []; // Associative arrays that become nested sections foreach ($data as $subKey => $subValue) { if (\is_array($subValue) && !$this->isInlineArray($subValue)) { + // Associative array becomes a nested section $subsections[$subKey] = $subValue; } else { + // Scalar or inline array stays in this section $simpleValues[$subKey] = $subValue; } } - // Only output the section header if there are simple values - // If there are only subsections, skip the intermediate section header + // Output the main section header only if there are simple values + // TOML requires section headers to have content, so we skip empty intermediate sections if (!empty($simpleValues)) { $toml .= "[{$sectionName}]\n"; foreach ($simpleValues as $key => $value) { @@ -224,7 +296,8 @@ private function sectionToToml(string $sectionName, array $data): string $toml .= "\n"; } - // Output subsections directly + // Recursively render nested subsections with dotted notation + // e.g., [section.subsection] format foreach ($subsections as $subKey => $subValue) { $toml .= $this->renderNestedSection("{$sectionName}.{$subKey}", $subValue); } @@ -244,29 +317,37 @@ private function renderNestedSection(string $sectionPath, array $data): string { $toml = ''; - // Separate simple values from subsections - $simpleValues = []; - $subsections = []; + // Recursively separate content into simple values and nested subsections + // This maintains proper TOML hierarchy for deeply nested configurations + $simpleValues = []; // Scalar values and inline arrays for this section + $subsections = []; // Nested associative arrays for deeper sections foreach ($data as $key => $value) { if (\is_array($value) && !$this->isInlineArray($value)) { + // Nested associative array - becomes a deeper subsection $subsections[$key] = $value; } else { + // Scalar value or inline array - stays at current nesting level $simpleValues[$key] = $value; } } - // Add section header if there are simple values OR if this section has no subsections - // (meaning it's an empty section that should be rendered) + // Render section header and content based on what we found + // Rules: + // 1. If we have simple values, create the section header and add them + // 2. If we have no subsections but the section exists, create empty section + // 3. If we only have subsections and no simple values, skip this level's header if (!empty($simpleValues) || empty($subsections)) { $toml .= "[{$sectionPath}]\n"; + // Output all simple key-value pairs for this section level foreach ($simpleValues as $key => $value) { $toml .= $this->formatKeyValue((string) $key, $value); } - $toml .= "\n"; + $toml .= "\n"; // Blank line after section content } - // Render subsections + // Recursively render all nested subsections with extended path + // Each subsection gets a dotted path like 'parent.child.grandchild' foreach ($subsections as $key => $value) { $toml .= $this->renderNestedSection("{$sectionPath}.{$key}", $value); } @@ -284,10 +365,13 @@ private function renderNestedSection(string $sectionPath, array $data): string private function formatKeyValue(string $key, mixed $value): string { if (\is_array($value)) { - // For array values, render them as inline arrays + // Array values become inline TOML arrays: key = ["item1", "item2"] + // Delegate to specialized array formatting method return "{$key} = " . $this->formatArrayValue($value) . "\n"; } + // Scalar values become quoted strings: key = "value" + // All values are quoted for consistency and safety return "{$key} = \"{$value}\"\n"; } @@ -300,16 +384,22 @@ private function formatKeyValue(string $key, mixed $value): string private function formatArrayValue(array $array): string { $items = []; + + // Convert each array element to a TOML-compatible string representation foreach ($array as $item) { if (\is_array($item)) { - // Nested arrays are not typically supported in TOML inline arrays - // Convert to string representation + // Nested arrays aren't natively supported in TOML inline arrays + // Serialize to JSON string as a workaround for complex data structures $items[] = '"' . \json_encode($item) . '"'; } else { + // Simple scalar values are converted to strings and quoted + // This ensures proper TOML syntax for all data types $items[] = '"' . (string) $item . '"'; } } + // Join all items with commas and wrap in square brackets + // Result: ["item1", "item2", "item3"] return '[' . \implode(', ', $items) . ']'; } @@ -321,23 +411,45 @@ private function formatArrayValue(array $array): string */ private function isInlineArray(array $array): bool { - // If empty, treat as section (will render as empty section) + // Empty arrays are treated as sections, not inline arrays + // This prevents creating empty inline arrays and ensures proper TOML structure if (empty($array)) { return false; } - // If all keys are numeric (sequential), it's an inline array + // Check if array has sequential numeric keys starting from 0 + // Only truly sequential arrays (0, 1, 2, ...) become inline TOML arrays + // Associative arrays with string keys become TOML sections instead return \array_keys($array) === \range(0, \count($array) - 1); } + /** + * Performs a deep merge of two arrays, recursively merging nested structures. + * + * When both arrays contain nested arrays at the same key, they are recursively + * merged rather than the second array completely replacing the first. For scalar + * values, the second array takes precedence (overlay behavior). + * + * @param array $array1 The base array (lower precedence) + * @param array $array2 The overlay array (higher precedence) + * @return array The merged result with nested structures preserved + */ private function deepMerge(array $array1, array $array2): array { - $merged = $array1; + $merged = $array1; // Start with base array as foundation + // Iterate through all keys in the second array (the overlay) foreach ($array2 as $key => $value) { + // Check if both the current value and existing value are arrays + // If so, recursively merge them instead of overwriting if (\is_array($value) && isset($merged[$key]) && \is_array($merged[$key])) { + // Recursive merge for nested array structures + // This preserves nested configuration while allowing overrides $merged[$key] = $this->deepMerge($merged[$key], $value); } else { + // For scalar values or when no existing array exists, simply overwrite + // This gives precedence to values from array2 (the overlay/remote config) + /** @var mixed */ $merged[$key] = $value; } } @@ -345,21 +457,43 @@ private function deepMerge(array $array1, array $array2): array return $merged; } + /** + * Sets a value at a nested array path using dot notation. + * + * Creates intermediate array levels as needed to accommodate the full path. + * If any intermediate level exists but is not an array, it will be converted + * to an empty array. The final value overwrites any existing value at the path. + * + * @param array $array The array to modify (passed by reference) + * @param non-empty-string $path The dot-notation path (e.g., "section.subsection.key") + * @param mixed $value The value to set at the specified path + */ private function setNestedValue(array &$array, string $path, mixed $value): void { + // Split dot-notation path into individual keys + // e.g., "github.plugins.logger" becomes ["github", "plugins", "logger"] $keys = \explode('.', $path); - $current = &$array; + $current = &$array; // Reference to current position in nested structure + // Navigate through the nested array structure, creating missing levels foreach ($keys as $key) { + // Ensure the current level is an array (convert if needed) if (!\is_array($current)) { $current = []; } + + // Create the key if it doesn't exist (initialize as empty array) if (!isset($current[$key])) { $current[$key] = []; } + + // Move reference deeper into the nested structure $current = &$current[$key]; } + // Set the final value at the deepest nesting level + // This overwrites any existing value at this path + /** @var mixed */ $current = $value; } } diff --git a/src/Module/Velox/Internal/ConfigBuilder.php b/src/Module/Velox/Internal/ConfigBuilder.php index 530ff45..fce96ab 100644 --- a/src/Module/Velox/Internal/ConfigBuilder.php +++ b/src/Module/Velox/Internal/ConfigBuilder.php @@ -54,7 +54,7 @@ public function buildConfig(VeloxAction $action, Path $buildDir): Path $configPath = $buildDir->join('velox.toml'); - \file_put_contents($configPath->__toString(), $configContent) or throw new ConfigException( + \file_put_contents($configPath->__toString(), $configContent) === false and throw new ConfigException( "Failed to write config file to: {$configPath}", ); diff --git a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php index f4e1284..c73a176 100644 --- a/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php +++ b/tests/Unit/Module/Velox/Internal/Config/Pipeline/TomlDataTest.php @@ -661,26 +661,6 @@ public function testToTomlOrdersSectionsWithRoadrunnerFirst(): void self::assertSame($expectedToml, $result); } - public function testToTomlNormalizesRoadrunnerRefWithVPrefix(): void - { - // Arrange - ref without "v" prefix - $data = [ - 'roadrunner' => ['ref' => '2025.1.1'], - ]; - $tomlData = new TomlData($data); - - // Act - $result = $tomlData->toToml(); - - // Assert - "v" prefix should be added - $expectedToml = << ['token' => 'secret'], 'log' => ['level' => 'info'], 'debug' => ['enabled' => 'false'], - 'roadrunner' => ['ref' => '2025.1.1'], + 'roadrunner' => ['ref' => 'v2025.1.1'], ]; $tomlData = new TomlData($data);