Skip to content

Commit 9c0566d

Browse files
committed
[path] Add runtime-aware tooling binary fallback
1 parent 7447cb2 commit 9c0566d

11 files changed

Lines changed: 248 additions & 12 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- 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)
13+
1014
## [1.24.1] - 2026-04-30
1115

1216
### Fixed

src/Console/Command/CodeStyleCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use FastForward\DevTools\Console\Input\HasJsonOption;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
2425
use FastForward\DevTools\Process\ProcessBuilderInterface;
2526
use FastForward\DevTools\Process\ProcessQueueInterface;
2627
use Psr\Log\LoggerInterface;
@@ -148,7 +149,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
148149
$processBuilder = $processBuilder->withArgument('--fix');
149150
}
150151

151-
$ecs = $processBuilder->build('vendor/bin/ecs');
152+
$ecs = $processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('ecs'));
152153

153154
$this->processQueue->add(process: $composerUpdate, label: 'Refreshing Composer Lock');
154155
$this->processQueue->add(

src/Console/Command/DependenciesCommand.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use FastForward\DevTools\Console\Input\HasJsonOption;
2424
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
25+
use FastForward\DevTools\Path\DevToolsPathResolver;
2526
use FastForward\DevTools\Process\ProcessBuilderInterface;
2627
use FastForward\DevTools\Process\ProcessQueueInterface;
2728
use InvalidArgumentException;
@@ -199,7 +200,9 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr
199200
}
200201

201202
$showShadowDependencies = (bool) $input->getOption('show-shadow-dependencies');
202-
$process = $processBuilder->build('vendor/bin/composer-dependency-analyser');
203+
$process = $processBuilder->build(
204+
DevToolsPathResolver::getPreferredToolBinaryPath('composer-dependency-analyser')
205+
);
203206
$process->setEnv([
204207
ComposerDependencyAnalyserConfig::ENV_SHOW_SHADOW_DEPENDENCIES => $showShadowDependencies ? '1' : '0',
205208
]);
@@ -217,7 +220,7 @@ private function getComposerDependencyAnalyserCommand(InputInterface $input): Pr
217220
*/
218221
private function getJackBreakpointCommand(InputInterface $input, int $maximumOutdated): Process
219222
{
220-
$command = 'vendor/bin/jack breakpoint';
223+
$command = DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' breakpoint';
221224

222225
if ((bool) $input->getOption('dev')) {
223226
$command .= ' --dev';
@@ -239,7 +242,7 @@ private function getJackBreakpointCommand(InputInterface $input, int $maximumOut
239242
*/
240243
private function getOpenVersionsCommand(InputInterface $input): Process
241244
{
242-
$command = 'vendor/bin/jack open-versions';
245+
$command = DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' open-versions';
243246

244247
if ((bool) $input->getOption('dev')) {
245248
$command .= ' --dev';
@@ -261,7 +264,7 @@ private function getOpenVersionsCommand(InputInterface $input): Process
261264
*/
262265
private function getRaiseToInstalledCommand(InputInterface $input): Process
263266
{
264-
$command = 'vendor/bin/jack raise-to-installed';
267+
$command = DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' raise-to-installed';
265268

266269
if ((bool) $input->getOption('dev')) {
267270
$command .= ' --dev';

src/Console/Command/PhpDocCommand.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use FastForward\DevTools\Console\Input\HasCacheOption;
2525
use FastForward\DevTools\Console\Input\HasJsonOption;
2626
use FastForward\DevTools\Filesystem\FilesystemInterface;
27+
use FastForward\DevTools\Path\DevToolsPathResolver;
2728
use FastForward\DevTools\Process\ProcessBuilderInterface;
2829
use FastForward\DevTools\Process\ProcessQueueInterface;
2930
use FastForward\DevTools\Path\ManagedWorkspace;
@@ -185,7 +186,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
185186
$processBuilder = $processBuilder->withArgument('--dry-run');
186187
}
187188

188-
$phpCsFixer = $processBuilder->build('vendor/bin/php-cs-fixer fix');
189+
$phpCsFixer = $processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer') . ' fix');
189190

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

209-
$rector = $processBuilder->build('vendor/bin/rector process');
210+
$rector = $processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('rector') . ' process');
210211

211212
$this->processQueue->add(process: $phpCsFixer, label: 'Fixing PHPDoc File Headers with PHP-CS-Fixer');
212213
$this->processQueue->add(process: $rector, label: 'Adding Missing PHPDoc with Rector');

src/Console/Command/RefactorCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use FastForward\DevTools\Console\Input\HasJsonOption;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
2425
use FastForward\DevTools\Process\ProcessBuilderInterface;
2526
use FastForward\DevTools\Process\ProcessQueueInterface;
2627
use Psr\Log\LoggerInterface;
@@ -143,7 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
143144
}
144145

145146
$this->processQueue->add(
146-
process: $processBuilder->build('vendor/bin/rector'),
147+
process: $processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('rector')),
147148
label: 'Refactoring Code with Rector',
148149
);
149150

src/Path/DevToolsPathResolver.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,52 @@ public static function getRuntimeAutoloadPath(string $packagePath = ''): string
108108
return Path::join($packagePath, 'vendor', 'autoload.php');
109109
}
110110

111+
/**
112+
* Returns the active Composer runtime binary path for the current DevTools installation mode.
113+
*
114+
* Repository checkouts use the package-local `vendor/bin/<binary>`, while
115+
* dependency installs resolve binaries from the active Composer vendor root.
116+
*
117+
* @param string $binary the binary name relative to `vendor/bin`
118+
* @param string $packagePath an optional package root path; defaults to the current package root
119+
*/
120+
public static function getRuntimeToolBinaryPath(string $binary, string $packagePath = ''): string
121+
{
122+
$packagePath = Path::canonicalize('' === $packagePath ? self::getPackagePath() : $packagePath);
123+
124+
if (self::isInstalledAsDependency($packagePath)) {
125+
return Path::canonicalize(Path::join($packagePath, '..', '..', 'bin', $binary));
126+
}
127+
128+
return Path::join($packagePath, 'vendor', 'bin', $binary);
129+
}
130+
131+
/**
132+
* Returns the preferred tooling binary path for the active project and DevTools runtime.
133+
*
134+
* Consumer projects SHOULD take precedence when they provide a local
135+
* `vendor/bin/<binary>` entry. If the binary is absent locally, the method
136+
* MUST fall back to the active DevTools runtime binary path.
137+
*
138+
* @param string $binary the binary name relative to `vendor/bin`
139+
* @param string $projectPath an optional project root path; defaults to the working project root
140+
* @param string $packagePath an optional package root path; defaults to the current package root
141+
*/
142+
public static function getPreferredToolBinaryPath(
143+
string $binary,
144+
string $projectPath = '',
145+
string $packagePath = '',
146+
): string {
147+
$projectPath = '' === $projectPath ? WorkingProjectPathResolver::getProjectPath() : $projectPath;
148+
$projectBinaryPath = Path::join($projectPath, 'vendor', 'bin', $binary);
149+
150+
if (file_exists($projectBinaryPath)) {
151+
return $projectBinaryPath;
152+
}
153+
154+
return self::getRuntimeToolBinaryPath($binary, $packagePath);
155+
}
156+
111157
/**
112158
* Detects whether the provided path belongs to an installed vendor copy of DevTools.
113159
*

tests/Console/Command/CodeStyleCommandTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121

2222
use FastForward\DevTools\Console\Command\CodeStyleCommand;
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
25+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2426
use FastForward\DevTools\Process\ProcessBuilderInterface;
2527
use FastForward\DevTools\Process\ProcessQueueInterface;
2628
use PHPUnit\Framework\Attributes\CoversClass;
2729
use PHPUnit\Framework\Attributes\Test;
30+
use PHPUnit\Framework\Attributes\UsesClass;
2831
use PHPUnit\Framework\Attributes\UsesTrait;
2932
use PHPUnit\Framework\TestCase;
3033
use Prophecy\Argument;
@@ -39,6 +42,8 @@
3942
use Symfony\Component\Process\Process;
4043

4144
#[CoversClass(CodeStyleCommand::class)]
45+
#[UsesClass(DevToolsPathResolver::class)]
46+
#[UsesClass(WorkingProjectPathResolver::class)]
4247
#[UsesTrait(LogsCommandResults::class)]
4348
final class CodeStyleCommandTest extends TestCase
4449
{
@@ -111,6 +116,9 @@ protected function setUp(): void
111116
#[Test]
112117
public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void
113118
{
119+
$this->processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('ecs'))
120+
->willReturn($this->process->reveal())
121+
->shouldBeCalled();
114122
$this->processQueue->run(Argument::type('object'))
115123
->willReturn(CodeStyleCommand::SUCCESS)
116124
->shouldBeCalled();

tests/Console/Command/DependenciesCommandTest.php

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2424
use FastForward\DevTools\Config\ComposerDependencyAnalyserConfig;
2525
use FastForward\DevTools\Path\DevToolsPathResolver;
26+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2627
use FastForward\DevTools\Process\ProcessBuilder;
2728
use FastForward\DevTools\Process\ProcessBuilderInterface;
2829
use FastForward\DevTools\Process\ProcessQueueInterface;
@@ -44,6 +45,7 @@
4445

4546
#[CoversClass(DependenciesCommand::class)]
4647
#[UsesClass(DevToolsPathResolver::class)]
48+
#[UsesClass(WorkingProjectPathResolver::class)]
4749
#[UsesClass(ProcessBuilder::class)]
4850
#[UsesTrait(LogsCommandResults::class)]
4951
final class DependenciesCommandTest extends TestCase
@@ -225,7 +227,9 @@ private function assertComposerDependencyAnalyserEnvironment(string $expectedVal
225227
$processBuilder->withArgument('--config', '/app/composer-dependency-analyser.php')
226228
->willReturn($configuredProcessBuilder->reveal())
227229
->shouldBeCalledOnce();
228-
$configuredProcessBuilder->build('vendor/bin/composer-dependency-analyser')
230+
$configuredProcessBuilder->build(
231+
DevToolsPathResolver::getPreferredToolBinaryPath('composer-dependency-analyser')
232+
)
229233
->willReturn($process->reveal())
230234
->shouldBeCalledOnce();
231235
$process->setEnv([
@@ -236,4 +240,75 @@ private function assertComposerDependencyAnalyserEnvironment(string $expectedVal
236240
(new ReflectionMethod($command, 'getComposerDependencyAnalyserCommand'))
237241
->invoke($command, $this->input->reveal());
238242
}
243+
244+
/**
245+
* @return void
246+
*/
247+
#[Test]
248+
public function jackBreakpointProcessWillUseTheResolvedJackBinary(): void
249+
{
250+
$processBuilder = $this->prophesize(ProcessBuilderInterface::class);
251+
$process = $this->prophesize(Process::class);
252+
$command = new DependenciesCommand(
253+
$processBuilder->reveal(),
254+
$this->processQueue->reveal(),
255+
$this->fileLocator->reveal(),
256+
$this->logger->reveal(),
257+
);
258+
259+
$processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' breakpoint --limit 5')
260+
->willReturn($process->reveal())
261+
->shouldBeCalledOnce();
262+
263+
(new ReflectionMethod($command, 'getJackBreakpointCommand'))
264+
->invoke($command, $this->input->reveal(), 5);
265+
}
266+
267+
/**
268+
* @return void
269+
*/
270+
#[Test]
271+
public function openVersionsProcessWillUseTheResolvedJackBinary(): void
272+
{
273+
$processBuilder = $this->prophesize(ProcessBuilderInterface::class);
274+
$process = $this->prophesize(Process::class);
275+
$command = new DependenciesCommand(
276+
$processBuilder->reveal(),
277+
$this->processQueue->reveal(),
278+
$this->fileLocator->reveal(),
279+
$this->logger->reveal(),
280+
);
281+
282+
$processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' open-versions --dry-run')
283+
->willReturn($process->reveal())
284+
->shouldBeCalledOnce();
285+
286+
(new ReflectionMethod($command, 'getOpenVersionsCommand'))
287+
->invoke($command, $this->input->reveal());
288+
}
289+
290+
/**
291+
* @return void
292+
*/
293+
#[Test]
294+
public function raiseToInstalledProcessWillUseTheResolvedJackBinary(): void
295+
{
296+
$processBuilder = $this->prophesize(ProcessBuilderInterface::class);
297+
$process = $this->prophesize(Process::class);
298+
$command = new DependenciesCommand(
299+
$processBuilder->reveal(),
300+
$this->processQueue->reveal(),
301+
$this->fileLocator->reveal(),
302+
$this->logger->reveal(),
303+
);
304+
305+
$processBuilder->build(
306+
DevToolsPathResolver::getPreferredToolBinaryPath('jack') . ' raise-to-installed --dry-run'
307+
)
308+
->willReturn($process->reveal())
309+
->shouldBeCalledOnce();
310+
311+
(new ReflectionMethod($command, 'getRaiseToInstalledCommand'))
312+
->invoke($command, $this->input->reveal());
313+
}
239314
}

tests/Console/Command/PhpDocCommandTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
use FastForward\DevTools\Console\Command\PhpDocCommand;
2929
use FastForward\DevTools\Console\Command\RefactorCommand;
3030
use FastForward\DevTools\Filesystem\FilesystemInterface;
31+
use FastForward\DevTools\Path\DevToolsPathResolver;
3132
use FastForward\DevTools\Process\ProcessBuilderInterface;
3233
use FastForward\DevTools\Process\ProcessQueueInterface;
3334
use FastForward\DevTools\Path\ManagedWorkspace;
35+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
3436
use PHPUnit\Framework\Attributes\CoversClass;
3537
use PHPUnit\Framework\Attributes\Test;
3638
use PHPUnit\Framework\Attributes\UsesClass;
@@ -53,6 +55,8 @@
5355
#[UsesClass(Author::class)]
5456
#[UsesClass(Support::class)]
5557
#[UsesClass(ManagedWorkspace::class)]
58+
#[UsesClass(DevToolsPathResolver::class)]
59+
#[UsesClass(WorkingProjectPathResolver::class)]
5660
#[UsesTrait(LogsCommandResults::class)]
5761
final class PhpDocCommandTest extends TestCase
5862
{
@@ -176,6 +180,12 @@ protected function setUp(): void
176180
public function executeWillCreateDocHeaderAndRunPhpDocProcesses(): void
177181
{
178182
$this->filesystem->dumpFile(PhpDocCommand::FILENAME, 'docheader')->shouldBeCalled();
183+
$this->processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer') . ' fix')
184+
->willReturn($this->process->reveal())
185+
->shouldBeCalled();
186+
$this->processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('rector') . ' process')
187+
->willReturn($this->process->reveal())
188+
->shouldBeCalled();
179189
$this->processBuilder->withArgument('--using-cache=yes')
180190
->willReturn($this->processBuilder->reveal())
181191
->shouldBeCalled();

tests/Console/Command/RefactorCommandTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121

2222
use FastForward\DevTools\Console\Command\RefactorCommand;
2323
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
24+
use FastForward\DevTools\Path\DevToolsPathResolver;
25+
use FastForward\DevTools\Path\WorkingProjectPathResolver;
2426
use FastForward\DevTools\Process\ProcessBuilderInterface;
2527
use FastForward\DevTools\Process\ProcessQueueInterface;
2628
use PHPUnit\Framework\Attributes\CoversClass;
2729
use PHPUnit\Framework\Attributes\Test;
30+
use PHPUnit\Framework\Attributes\UsesClass;
2831
use PHPUnit\Framework\Attributes\UsesTrait;
2932
use PHPUnit\Framework\TestCase;
3033
use Prophecy\Argument;
@@ -39,6 +42,8 @@
3942
use Symfony\Component\Process\Process;
4043

4144
#[CoversClass(RefactorCommand::class)]
45+
#[UsesClass(DevToolsPathResolver::class)]
46+
#[UsesClass(WorkingProjectPathResolver::class)]
4247
#[UsesTrait(LogsCommandResults::class)]
4348
final class RefactorCommandTest extends TestCase
4449
{
@@ -108,6 +113,9 @@ protected function setUp(): void
108113
#[Test]
109114
public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void
110115
{
116+
$this->processBuilder->build(DevToolsPathResolver::getPreferredToolBinaryPath('rector'))
117+
->willReturn($this->process->reveal())
118+
->shouldBeCalled();
111119
$this->processQueue->run($this->output->reveal())
112120
->willReturn(RefactorCommand::SUCCESS)
113121
->shouldBeCalled();

0 commit comments

Comments
 (0)