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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from 4af528 to 7f52fa
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Prefer project-local tooling binaries when available and fall back to the active DevTools runtime for `php-cs-fixer`, Rector, ECS, Jack, and Composer Dependency Analyser during global `dev-tools` runs (#292)

## [1.24.1] - 2026-04-30

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion src/Console/Command/CodeStyleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
use FastForward\DevTools\Console\Input\HasJsonOption;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -148,7 +149,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--fix');
}

$ecs = $processBuilder->build('vendor/bin/ecs');
$ecs = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('ecs')]);

$this->processQueue->add(process: $composerUpdate, label: 'Refreshing Composer Lock');
$this->processQueue->add(
Expand Down
29 changes: 16 additions & 13 deletions src/Console/Command/DependenciesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
use FastForward\DevTools\Console\Input\HasJsonOption;
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use InvalidArgumentException;
Expand Down Expand Up @@ -199,7 +200,9 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr
}

$showShadowDependencies = (bool) $input->getOption('show-shadow-dependencies');
$process = $processBuilder->build('vendor/bin/composer-dependency-analyser');
$process = $processBuilder->build(
[DevToolsPathResolver::getPreferredToolBinaryPath('composer-dependency-analyser')]
);
$process->setEnv([
ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $showShadowDependencies ? '1' : '0',
]);
Expand All @@ -217,17 +220,17 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr
*/
private function getJackBreakpointCommand(InputInterface $input, int $maximumOutdated): Process
{
$command = 'vendor/bin/jack breakpoint';
$processBuilder = $this->processBuilder;

if ((bool) $input->getOption('dev')) {
$command .= ' --dev';
$processBuilder = $processBuilder->withArgument('--dev');
}

if (! $this->shouldIgnoreOutdatedFailures($maximumOutdated)) {
$command .= ' --limit ' . $maximumOutdated;
$processBuilder = $processBuilder->withArgument('--limit', (string) $maximumOutdated);
}

return $this->processBuilder->build($command);
return $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('jack'), 'breakpoint']);
}

/**
Expand All @@ -239,17 +242,17 @@ private function getJackBreakpointCommand(InputInterface $input, int $maximumOut
*/
private function getOpenVersionsCommand(InputInterface $input): Process
{
$command = 'vendor/bin/jack open-versions';
$processBuilder = $this->processBuilder;

if ((bool) $input->getOption('dev')) {
$command .= ' --dev';
$processBuilder = $processBuilder->withArgument('--dev');
}

if (! (bool) $input->getOption('upgrade')) {
$command .= ' --dry-run';
$processBuilder = $processBuilder->withArgument('--dry-run');
}

return $this->processBuilder->build($command);
return $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('jack'), 'open-versions']);
}

/**
Expand All @@ -261,17 +264,17 @@ private function getOpenVersionsCommand(InputInterface $input): Process
*/
private function getRaiseToInstalledCommand(InputInterface $input): Process
{
$command = 'vendor/bin/jack raise-to-installed';
$processBuilder = $this->processBuilder;

if ((bool) $input->getOption('dev')) {
$command .= ' --dev';
$processBuilder = $processBuilder->withArgument('--dev');
}

if (! (bool) $input->getOption('upgrade')) {
$command .= ' --dry-run';
$processBuilder = $processBuilder->withArgument('--dry-run');
}

return $this->processBuilder->build($command);
return $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('jack'), 'raise-to-installed']);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/Console/Command/PhpDocCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use FastForward\DevTools\Console\Input\HasCacheOption;
use FastForward\DevTools\Console\Input\HasJsonOption;
use FastForward\DevTools\Filesystem\FilesystemInterface;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use FastForward\DevTools\Path\ManagedWorkspace;
Expand Down Expand Up @@ -185,7 +186,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--dry-run');
}

$phpCsFixer = $processBuilder->build('vendor/bin/php-cs-fixer fix');
$phpCsFixer = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer'), 'fix']);

$processBuilder = $this->processBuilder
->withArgument('--ansi')
Expand All @@ -206,7 +207,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--dry-run');
}

$rector = $processBuilder->build('vendor/bin/rector process');
$rector = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('rector'), 'process']);

$this->processQueue->add(process: $phpCsFixer, label: 'Fixing PHPDoc File Headers with PHP-CS-Fixer');
$this->processQueue->add(process: $rector, label: 'Adding Missing PHPDoc with Rector');
Expand Down
4 changes: 2 additions & 2 deletions src/Console/Command/RefactorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
use FastForward\DevTools\Console\Input\HasJsonOption;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -124,7 +125,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
]);

$processBuilder = $this->processBuilder
->withArgument('process')
->withArgument('--ansi')
->withArgument('--config')
->withArgument($this->fileLocator->locate(self::CONFIG));
Expand All @@ -143,7 +143,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$this->processQueue->add(
process: $processBuilder->build('vendor/bin/rector'),
process: $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('rector'), 'process']),
label: 'Refactoring Code with Rector',
);

Expand Down
6 changes: 3 additions & 3 deletions src/Console/Command/ReportsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$docsBuilder = $docsBuilder->withArgument('--pretty-json');
}

$docs = $docsBuilder->build(DevToolsPathResolver::getBinaryCommand('docs'));
$docs = $docsBuilder->build([DevToolsPathResolver::getBinaryPath(), 'docs']);

if (null !== $cacheArgument) {
$coverageBuilder = $coverageBuilder->withArgument($cacheArgument);
Expand All @@ -192,7 +192,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$coverageBuilder = $coverageBuilder->withArgument('--pretty-json');
}

$coverage = $coverageBuilder->build(DevToolsPathResolver::getBinaryCommand('tests'));
$coverage = $coverageBuilder->build([DevToolsPathResolver::getBinaryPath(), 'tests']);

if ($progress) {
$metricsBuilder = $metricsBuilder->withArgument('--progress');
Expand All @@ -206,7 +206,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$metricsBuilder = $metricsBuilder->withArgument('--pretty-json');
}

$metrics = $metricsBuilder->build(DevToolsPathResolver::getBinaryCommand('metrics'));
$metrics = $metricsBuilder->build([DevToolsPathResolver::getBinaryPath(), 'metrics']);

$this->processQueue->add(process: $docs, detached: true, label: 'Generating API Docs Report');
$this->processQueue->add(process: $coverage, label: 'Generating Coverage Report');
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Command/StandardsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$this->processQueue->add(
process: $processBuilder->build(DevToolsPathResolver::getBinaryCommand($command)),
process: $processBuilder->build([DevToolsPathResolver::getBinaryPath(), $command]),
label: $this->getProcessLabel($command),
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Console/Command/SyncCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private function queueDevToolsCommand(
$processBuilder = $processBuilder->withArgument($argument);
}

$process = $processBuilder->build(DevToolsPathResolver::getBinaryPath());
$process = $processBuilder->build([DevToolsPathResolver::getBinaryPath()]);

$this->processQueue->add(process: $process, detached: $detached, label: 'Running DevTools Sync Hook');
}
Expand Down
46 changes: 46 additions & 0 deletions src/Path/DevToolsPathResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,52 @@ public static function getRuntimeAutoloadPath(string $packagePath = ''): string
return Path::join($packagePath, 'vendor', 'autoload.php');
}

/**
* Returns the active Composer runtime binary path for the current DevTools installation mode.
*
* Repository checkouts use the package-local `vendor/bin/<binary>`, while
* dependency installs resolve binaries from the active Composer vendor root.
*
* @param string $binary the binary name relative to `vendor/bin`
* @param string $packagePath an optional package root path; defaults to the current package root
*/
public static function getRuntimeToolBinaryPath(string $binary, string $packagePath = ''): string
{
$packagePath = Path::canonicalize('' === $packagePath ? self::getPackagePath() : $packagePath);

if (self::isInstalledAsDependency($packagePath)) {
return Path::canonicalize(Path::join($packagePath, '..', '..', 'bin', $binary));
}

return Path::join($packagePath, 'vendor', 'bin', $binary);
}

/**
* Returns the preferred tooling binary path for the active project and DevTools runtime.
*
* Consumer projects SHOULD take precedence when they provide a local
* `vendor/bin/<binary>` entry. If the binary is absent locally, the method
* MUST fall back to the active DevTools runtime binary path.
*
* @param string $binary the binary name relative to `vendor/bin`
* @param string $projectPath an optional project root path; defaults to the working project root
* @param string $packagePath an optional package root path; defaults to the current package root
*/
public static function getPreferredToolBinaryPath(
string $binary,
string $projectPath = '',
string $packagePath = '',
): string {
$projectPath = '' === $projectPath ? WorkingProjectPathResolver::getProjectPath() : $projectPath;
$projectBinaryPath = Path::join($projectPath, 'vendor', 'bin', $binary);

if (file_exists($projectBinaryPath)) {
return $projectBinaryPath;
}

return self::getRuntimeToolBinaryPath($binary, $packagePath);
}

/**
* Detects whether the provided path belongs to an installed vendor copy of DevTools.
*
Expand Down
8 changes: 8 additions & 0 deletions tests/Console/Command/CodeStyleCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@

use FastForward\DevTools\Console\Command\CodeStyleCommand;
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Path\WorkingProjectPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessQueueInterface;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\Attributes\UsesTrait;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
Expand All @@ -39,6 +42,8 @@
use Symfony\Component\Process\Process;

#[CoversClass(CodeStyleCommand::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(WorkingProjectPathResolver::class)]
#[UsesTrait(LogsCommandResults::class)]
final class CodeStyleCommandTest extends TestCase
{
Expand Down Expand Up @@ -111,6 +116,9 @@ protected function setUp(): void
#[Test]
public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void
{
$this->processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('ecs')])
->willReturn($this->process->reveal())
->shouldBeCalled();
$this->processQueue->run(Argument::type('object'))
->willReturn(CodeStyleCommand::SUCCESS)
->shouldBeCalled();
Expand Down
Loading
Loading