diff --git a/README.md b/README.md
index 6dfdf21..7a2a01d 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ This repository contains commands to help you work with the RoadRunner, such as:
Using the `preset` option can create an example configuration file with popular plugins for different typical tasks.
For example, with web preset: `get-binary --preset web`.
Available presets: `web` (contains plugins `http`, `jobs`).
+- `download-protoc-binary` - allows to install the latest version of the `protoc-gen-php-grpc` file compatible with
+ your environment (operating system, processor architecture, runtime, etc...).
- `versions` - displays a list of available RoadRunner binary versions.
Testing:
diff --git a/bin/rr b/bin/rr
index c01b3c1..6156730 100644
--- a/bin/rr
+++ b/bin/rr
@@ -10,6 +10,7 @@
declare(strict_types=1);
+use Spiral\RoadRunner\Console\DownloadProtocBinaryCommand;
use Spiral\RoadRunner\Console\GetBinaryCommand;
use Spiral\RoadRunner\Console\VersionsCommand;
use Spiral\RoadRunner\Version;
@@ -68,5 +69,6 @@ $app = new Symfony\Component\Console\Application('RoadRunner CLI', Version::curr
$app->add(new GetBinaryCommand());
$app->add(new VersionsCommand());
+$app->add(new DownloadProtocBinaryCommand());
$app->run();
diff --git a/src/DownloadProtocBinaryCommand.php b/src/DownloadProtocBinaryCommand.php
new file mode 100644
index 0000000..6c89569
--- /dev/null
+++ b/src/DownloadProtocBinaryCommand.php
@@ -0,0 +1,233 @@
+os = new OperatingSystemOption($this);
+ $this->arch = new ArchitectureOption($this);
+ $this->version = new VersionFilterOption($this);
+ $this->location = new InstallationLocationOption($this);
+ $this->stability = new StabilityOption($this);
+ }
+
+ public function getDescription(): string
+ {
+ return 'Install or update protoc-gen-php-grpc binary';
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = $this->io($input, $output);
+
+ $target = $this->location->get($input, $io);
+ $repository = $this->getRepository();
+
+ $output->writeln('');
+ $output->writeln(' Environment:');
+ $output->writeln(\sprintf(' - Version: %s', $this->version->get($input, $io)));
+ $output->writeln(\sprintf(' - Stability: %s', $this->stability->get($input, $io)));
+ $output->writeln(\sprintf(' - Operating System: %s', $this->os->get($input, $io)));
+ $output->writeln(\sprintf(' - Architecture: %s', $this->arch->get($input, $io)));
+ $output->writeln('');
+
+ // List of all available releases
+ $releases = $this->version->find($input, $io, $repository);
+
+ /**
+ * @var AssetInterface $asset
+ * @var ReleaseInterface $release
+ */
+ [$asset, $release] = $this->findAsset($repository, $releases, $input, $io);
+
+ // Installation
+ $output->writeln(
+ \sprintf(" - %s", $release->getRepositoryName()) .
+ \sprintf(' (%s):', $release->getVersion()) .
+ ' Downloading...'
+ );
+
+ if ($output->isVerbose()) {
+ $output->writeln(\sprintf(" -- %s", $asset->getName()));
+ }
+
+ // Install rr binary
+ $file = $this->installBinary($target, $release, $asset, $io, $output);
+
+ // Success
+ if ($file === null) {
+ $io->warning('protoc-gen-php-grpc has not been installed');
+
+ return 1;
+ }
+
+ return 0;
+ }
+
+ private function installBinary(
+ string $target,
+ ReleaseInterface $release,
+ AssetInterface $asset,
+ StyleInterface $io,
+ OutputInterface $out
+ ): ?\SplFileInfo {
+ $extractor = $this->assetToArchive($asset, $out)
+ ->extract([
+ 'protoc-gen-php-grpc.exe' => $target . '/protoc-gen-php-grpc.exe',
+ 'protoc-gen-php-grpc' => $target . '/protoc-gen-php-grpc',
+ ]);
+
+ $file = null;
+ while ($extractor->valid()) {
+ $file = $extractor->current();
+
+ if (!$this->checkExisting($file, $io)) {
+ $extractor->send(false);
+ continue;
+ }
+
+ // Success
+ $path = $file->getRealPath() ?: $file->getPathname();
+ $message = 'protoc-gen-php-grpc (%s) has been installed into %s';
+ $message = \sprintf($message, $release->getVersion(), $path);
+ $out->writeln($message);
+
+ $extractor->next();
+
+ if (!$file->isExecutable()) {
+ @chmod($file->getRealPath(), 0755);
+ }
+ }
+
+ return $file;
+ }
+
+ private function checkExisting(\SplFileInfo $bin, StyleInterface $io): bool
+ {
+ if (\is_file($bin->getPathname())) {
+ $io->warning('protoc-gen-php-grpc binary file already exists!');
+
+ if (!$io->confirm('Do you want overwrite it?', false)) {
+ $io->note('Skipping protoc-gen-php-grpc installation...');
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private function findAsset(
+ RepositoryInterface $repo,
+ ReleasesCollection $releases,
+ InputInterface $in,
+ StyleInterface $io
+ ): array {
+ $osOption = $this->os->get($in, $io);
+ $archOption = $this->arch->get($in, $io);
+ $stabilityOption = $this->stability->get($in, $io);
+
+ /** @var ReleaseInterface[] $filtered */
+ $filtered = $releases
+ ->minimumStability($stabilityOption)
+ ->withAssets();
+
+ foreach ($filtered as $release) {
+ $asset = $release->getAssets()
+ ->filter(static fn (AssetInterface $asset): bool =>
+ 0 === \strncmp($asset->getName(), 'protoc-gen-php-grpc', \strlen('protoc-gen-php-grpc'))
+ )
+ ->whereArchitecture($archOption)
+ ->whereOperatingSystem($osOption)
+ ->first();
+
+ if ($asset === null) {
+ $io->warning(
+ \vsprintf('%s %s does not contain available assembly (further search in progress)', [
+ $repo->getName(),
+ $release->getVersion(),
+ ])
+ );
+
+ continue;
+ }
+
+ return [$asset, $release];
+ }
+
+ $message = \vsprintf(self::ERROR_ENVIRONMENT, [
+ $this->os->getName(),
+ $osOption,
+ $this->arch->getName(),
+ $archOption,
+ $this->stability->getName(),
+ $stabilityOption,
+ $this->version->choices($releases),
+ ]);
+
+ throw new \UnexpectedValueException($message);
+ }
+
+ private function assetToArchive(AssetInterface $asset, OutputInterface $out, string $temp = null): ArchiveInterface
+ {
+ $factory = new Factory();
+
+ $progress = new ProgressBar($out);
+ $progress->setFormat(' [%bar%] %percent:3s%% (%size%Kb/%total%Kb)');
+ $progress->setMessage('0.00', 'size');
+ $progress->setMessage('?.??', 'total');
+ $progress->display();
+
+ try {
+ return $factory->fromAsset($asset, function (int $size, int $total) use ($progress) {
+ if ($progress->getMaxSteps() !== $total) {
+ $progress->setMaxSteps($total);
+ }
+
+ if ($progress->getStartTime() === 0) {
+ $progress->start();
+ }
+
+ $progress->setMessage(\number_format($size / 1000, 2), 'size');
+ $progress->setMessage(\number_format($total / 1000, 2), 'total');
+
+ $progress->setProgress($size);
+ }, $temp);
+ } finally {
+ $progress->clear();
+ }
+ }
+}