Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/buid_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ jobs:
cp -R build/target-repository/. ecs-prefixed-downgraded
cp -R templates ecs-prefixed-downgraded/

# 5b. bundle ecs-go binaries for --turbo, so it works without a Go toolchain
-
uses: actions/setup-go@v5
with:
go-version: "1.26.6"

- run: |
git clone --depth 1 https://github.com/TomasVotruba/ecs-go.git ecs-go-local
mkdir -p ecs-prefixed-downgraded/bin/turbo

for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do
(cd ecs-go-local/ecs-go && CGO_ENABLED=0 GOOS=${target%/*} GOARCH=${target#*/} go build -trimpath -ldflags="-s -w" -o "$GITHUB_WORKSPACE/ecs-prefixed-downgraded/bin/turbo/ecs-go-${target%/*}-${target#*/}" .)
done

chmod 755 ecs-prefixed-downgraded/bin/turbo/*
rm -rf ecs-go-local

# 6. clone remote repository, so we can push it
-
uses: "actions/checkout@v4"
Expand Down
16 changes: 7 additions & 9 deletions docs/turbo.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,16 @@ Configured fixer options are read off the fixer instance; sniff properties are n
The ecs-go binary is looked up in this order:

1. the `ECS_TURBO_BIN` environment variable, if it points to an existing file;
2. `vendor/bin/ecs-go`, if present;
3. `ecs-go` on the `PATH`.
2. the binary bundled with ECS in `bin/turbo/` (Linux and macOS, amd64 and arm64);
3. `vendor/bin/ecs-go`, if present;
4. `ecs-go` on the `PATH`.

If none is found, `--turbo` fails with an error explaining how to provide the binary.

## Prototype caveat

This is an RFC-stage prototype. ecs-go reads the rules and skips from your `ecs.php` (not just the paths), and because every ecs-go fixer is named by its PHP-CS-Fixer class, the rules map straight across by name. Every rule ecs-go has no fixer for is reported and skipped, and a configured rule runs with ecs-go's built-in behaviour (its configuration is not modelled yet) and is noted in the report. So a turbo run is never silently narrower than your config, but it is not yet equivalent to a full ECS run.

## Remaining delivery work

For `--turbo` to work out of the box, the ecs-go binary has to be installed alongside ECS. The planned path:

- require `tomasvotruba/ecs-go` as a dev dependency, which exposes `vendor/bin/ecs-go`;
- or ship ecs-go as downloadable per-OS binaries and point `ECS_TURBO_BIN` at one.
## Bundled binary

Until then, build ecs-go yourself and point `ECS_TURBO_BIN` at it, or put it on your `PATH`.
The release build cross-compiles ecs-go from its `main` branch and ships it in `bin/turbo/`, so `--turbo` works out of the box on Linux and macOS - no Go toolchain needed. Windows is not bundled yet, as ecs-go does not build there; use `ECS_TURBO_BIN` or WSL.
58 changes: 55 additions & 3 deletions src/Turbo/EcsGoBinaryLocator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,80 @@

namespace Symplify\EasyCodingStandard\Turbo;

use Symplify\EasyCodingStandard\Turbo\Exception\EcsGoBinaryNotFoundException;

/**
* Resolves the "ecs-go" Go binary that powers the experimental --turbo mode.
*
* @see \Symplify\EasyCodingStandard\Tests\Turbo\EcsGoBinaryLocatorTest
*/
final class EcsGoBinaryLocator
final readonly class EcsGoBinaryLocator
{
private const string ENV_OVERRIDE = 'ECS_TURBO_BIN';

public function __construct(
// filled with per-platform binaries by the release build, see .github/workflows/buid_release.yaml
private string $bundledDirectory = __DIR__ . '/../../bin/turbo',
) {
}

public function locate(): string
{
$envBinary = getenv(self::ENV_OVERRIDE);
if (is_string($envBinary) && $envBinary !== '' && is_file($envBinary)) {
return $envBinary;
}

$bundledBinary = $this->bundledDirectory . '/ecs-go-' . $this->resolvePlatform();
if (is_file($bundledBinary)) {
// archive extraction can drop the executable bit
if (! is_executable($bundledBinary)) {
chmod($bundledBinary, 0755);
}

return $bundledBinary;
}

$vendorBinary = getcwd() . '/vendor/bin/ecs-go';
if (is_file($vendorBinary)) {
return $vendorBinary;
}

// fall back to the binary on PATH
return 'ecs-go';
$pathBinary = $this->findOnPath('ecs-go');
if ($pathBinary !== null) {
return $pathBinary;
}

throw new EcsGoBinaryNotFoundException(sprintf(
'The ecs-go binary for --turbo was not found in "%s" env, "vendor/bin/ecs-go" or on PATH. Build it from https://github.com/TomasVotruba/ecs-go and point "%s" to it.',
self::ENV_OVERRIDE,
self::ENV_OVERRIDE,
));
}

private function resolvePlatform(): string
{
$machine = strtolower(php_uname('m'));
$arch = in_array($machine, ['aarch64', 'arm64'], true) ? 'arm64' : 'amd64';

return strtolower(PHP_OS_FAMILY) . '-' . $arch;
}

private function findOnPath(string $binaryName): ?string
{
$path = (string) getenv('PATH');

foreach (explode(PATH_SEPARATOR, $path) as $directory) {
if ($directory === '') {
continue;
}

$candidate = $directory . DIRECTORY_SEPARATOR . $binaryName;
if (is_file($candidate) && is_executable($candidate)) {
return $candidate;
}
}

return null;
}
}
11 changes: 11 additions & 0 deletions src/Turbo/Exception/EcsGoBinaryNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Turbo\Exception;

use Exception;

final class EcsGoBinaryNotFoundException extends Exception
{
}
45 changes: 43 additions & 2 deletions tests/Turbo/EcsGoBinaryLocatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\TestCase;
use Symplify\EasyCodingStandard\Turbo\EcsGoBinaryLocator;
use Symplify\EasyCodingStandard\Turbo\Exception\EcsGoBinaryNotFoundException;

final class EcsGoBinaryLocatorTest extends TestCase
{
Expand All @@ -25,10 +26,50 @@ public function testEnvironmentOverrideWins(): void
putenv('ECS_TURBO_BIN');
}

public function testFallsBackToPathWhenNoBinaryFound(): void
public function testBundledBinaryWinsOverPath(): void
{
putenv('ECS_TURBO_BIN');

$this->assertSame('ecs-go', $this->ecsGoBinaryLocator->locate());
$bundledDirectory = sys_get_temp_dir() . '/ecs-turbo-bundled-' . uniqid();
mkdir($bundledDirectory);

$arch = in_array(strtolower(php_uname('m')), ['aarch64', 'arm64'], true) ? 'arm64' : 'amd64';
$bundledBinary = $bundledDirectory . '/ecs-go-' . strtolower(PHP_OS_FAMILY) . '-' . $arch;
touch($bundledBinary);

$ecsGoBinaryLocator = new EcsGoBinaryLocator($bundledDirectory);

try {
$this->assertSame($bundledBinary, $ecsGoBinaryLocator->locate());
$this->assertTrue(is_executable($bundledBinary));
} finally {
unlink($bundledBinary);
rmdir($bundledDirectory);
}
}

public function testFindsBinaryOnPath(): void
{
putenv('ECS_TURBO_BIN');
$originalPath = (string) getenv('PATH');
putenv('PATH=' . __DIR__ . '/Source/bin');

$this->assertSame(__DIR__ . '/Source/bin/ecs-go', $this->ecsGoBinaryLocator->locate());

putenv('PATH=' . $originalPath);
}

public function testThrowsWhenNoBinaryFound(): void
{
putenv('ECS_TURBO_BIN');
$originalPath = (string) getenv('PATH');
putenv('PATH=');

try {
$this->expectException(EcsGoBinaryNotFoundException::class);
$this->ecsGoBinaryLocator->locate();
} finally {
putenv('PATH=' . $originalPath);
}
}
}
1 change: 1 addition & 0 deletions tests/Turbo/Source/bin/ecs-go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/bin/sh
Loading