Skip to content

Commit fb4deb6

Browse files
committed
Infer self-update scope from installation
1 parent c744f03 commit fb4deb6

10 files changed

Lines changed: 334 additions & 36 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ skills they depend on.
294294
| `composer codeowners` | Generates managed `.github/CODEOWNERS` content from local repository metadata. |
295295
| `composer gitattributes` | Manages export-ignore rules in `.gitattributes`. |
296296
| `composer dev-tools:sync` | Updates scripts, CODEOWNERS, funding metadata, workflow stubs, `.editorconfig`, `.gitignore`, `.gitattributes`, wiki setup, packaged skills, and packaged agents. |
297-
| `vendor/bin/dev-tools self-update` / `composer dev-tools:self-update` | Updates the local DevTools package, or the Composer global installation with `--global`. |
297+
| `vendor/bin/dev-tools self-update` / `composer dev-tools:self-update` | Updates the local DevTools package, or the Composer global installation when the active binary is globally installed. |
298298

299299
## 🔌 Integration
300300

docs/commands/self-update.rst

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,12 @@ Usage
1010
.. code-block:: bash
1111
1212
vendor/bin/dev-tools self-update
13-
vendor/bin/dev-tools self-update --global
1413
composer dev-tools:self-update
1514
16-
Options
17-
-------
18-
19-
.. list-table::
20-
:header-rows: 1
21-
:widths: 24 76
22-
23-
* - Option
24-
- Description
25-
* - ``--global``
26-
- Run ``composer global update fast-forward/dev-tools`` instead of
27-
updating the current project installation.
15+
When the standalone ``dev-tools`` binary is itself loaded from Composer's
16+
global installation, ``self-update`` automatically targets
17+
``composer global update fast-forward/dev-tools``. Local project
18+
installations update the current project by default.
2819

2920
Global runtime options
3021
----------------------
@@ -42,11 +33,11 @@ paths, managed files, or command defaults. This lets a globally installed
4233
binary operate on another project without first changing shell directories.
4334
Composer executions can use Composer's own ``--working-dir``/``-d`` option.
4435

45-
``--auto-update`` runs the project self-update flow before the requested
46-
command. The same behavior MAY be enabled with ``FAST_FORWARD_AUTO_UPDATE``;
47-
set it to ``global`` when the update should target Composer's global
48-
installation. Auto-update failures are reported as warnings and do not block
49-
the requested command.
36+
``--auto-update`` runs the self-update flow before the requested command. The
37+
same behavior MAY be enabled with ``FAST_FORWARD_AUTO_UPDATE=1``. When the
38+
active ``dev-tools`` binary is already installed globally, auto-update also
39+
targets the global installation by default. Auto-update failures are reported
40+
as warnings and do not block the requested command.
5041

5142
Version freshness check
5243
-----------------------

src/Console/Command/SelfUpdateCommand.php

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2323
use FastForward\DevTools\Reflection\ClassReflection;
2424
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
25+
use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
2526
use Psr\Log\LoggerInterface;
2627
use Symfony\Component\Console\Attribute\AsCommand;
2728
use Symfony\Component\Console\Command\Command;
2829
use Symfony\Component\Console\Input\InputInterface;
29-
use Symfony\Component\Console\Input\InputOption;
3030
use Symfony\Component\Console\Output\OutputInterface;
3131

3232
/**
@@ -43,10 +43,12 @@ final class SelfUpdateCommand extends Command
4343

4444
/**
4545
* @param SelfUpdateRunnerInterface $selfUpdateRunner the runner that executes Composer's update command
46+
* @param SelfUpdateScopeResolverInterface $scopeResolver resolves whether the active binary is globally installed
4647
* @param LoggerInterface $logger the output-aware logger
4748
*/
4849
public function __construct(
4950
private readonly SelfUpdateRunnerInterface $selfUpdateRunner,
51+
private readonly SelfUpdateScopeResolverInterface $scopeResolver,
5052
private readonly LoggerInterface $logger,
5153
) {
5254
parent::__construct();
@@ -80,14 +82,9 @@ public static function getCommandNames(): array
8082
protected function configure(): void
8183
{
8284
$this->setHelp(
83-
'This command updates fast-forward/dev-tools through Composer. By default it updates the current'
84-
. ' project installation; use --global for Composer global installations.'
85-
);
86-
87-
$this->addOption(
88-
name: 'global',
89-
mode: InputOption::VALUE_NONE,
90-
description: 'Update the Composer global fast-forward/dev-tools installation.',
85+
'This command updates fast-forward/dev-tools through Composer. When DevTools is running from'
86+
. ' Composer global, the command updates that global installation automatically; otherwise it updates'
87+
. ' the current project installation.'
9188
);
9289
}
9390

@@ -101,7 +98,7 @@ protected function configure(): void
10198
*/
10299
protected function execute(InputInterface $input, OutputInterface $output): int
103100
{
104-
$global = (bool) $input->getOption('global');
101+
$global = $this->scopeResolver->isGlobalInstallation();
105102

106103
$this->logger->info('Updating DevTools installation...', [
107104
'input' => $input,

src/Console/DevTools.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Override;
2424
use FastForward\DevTools\Environment\EnvironmentInterface;
2525
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
26+
use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
2627
use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface;
2728
use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface;
2829
use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider;
@@ -66,13 +67,15 @@ final class DevTools extends Application
6667
* @param WorkingDirectorySwitcherInterface $workingDirectorySwitcher switches the process working directory
6768
* @param VersionCheckNotifierInterface $versionCheckNotifier emits non-blocking version freshness warnings
6869
* @param SelfUpdateRunnerInterface $selfUpdateRunner runs explicit or automatic self-update flows
70+
* @param SelfUpdateScopeResolverInterface $selfUpdateScopeResolver resolves whether the active binary is global
6971
* @param EnvironmentInterface $environment reads environment flags for optional auto-update behavior
7072
*/
7173
public function __construct(
7274
CommandLoaderInterface $commandLoader,
7375
private readonly WorkingDirectorySwitcherInterface $workingDirectorySwitcher,
7476
private readonly VersionCheckNotifierInterface $versionCheckNotifier,
7577
private readonly SelfUpdateRunnerInterface $selfUpdateRunner,
78+
private readonly SelfUpdateScopeResolverInterface $selfUpdateScopeResolver,
7679
private readonly EnvironmentInterface $environment,
7780
) {
7881
parent::__construct('Fast Forward Dev Tools');
@@ -195,7 +198,8 @@ private function runAutoUpdateWhenRequested(InputInterface $input, OutputInterfa
195198
}
196199

197200
try {
198-
$statusCode = $this->selfUpdateRunner->update('global' === $autoUpdateMode, $output);
201+
$global = $this->selfUpdateScopeResolver->isGlobalInstallation();
202+
$statusCode = $this->selfUpdateRunner->update($global, $output);
199203
} catch (Throwable) {
200204
$output->writeln('<comment>DevTools auto-update failed; continuing with the requested command.</comment>');
201205

@@ -224,6 +228,6 @@ private function isSelfUpdateCommand(InputInterface $input): bool
224228
*/
225229
private function isTruthyAutoUpdateMode(?string $mode): bool
226230
{
227-
return null !== $mode && \in_array(strtolower($mode), ['1', 'true', 'yes', 'on', 'global'], true);
231+
return null !== $mode && \in_array(strtolower($mode), ['1', 'true', 'yes', 'on'], true);
228232
}
229233
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Fast Forward Development Tools for PHP projects.
7+
*
8+
* This file is part of fast-forward/dev-tools project.
9+
*
10+
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
11+
* @license https://opensource.org/licenses/MIT MIT License
12+
*
13+
* @see https://github.com/php-fast-forward/
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward/dev-tools/issues
16+
* @see https://php-fast-forward.github.io/dev-tools/
17+
* @see https://datatracker.ietf.org/doc/html/rfc2119
18+
*/
19+
20+
namespace FastForward\DevTools\SelfUpdate;
21+
22+
use FastForward\DevTools\Environment\EnvironmentInterface;
23+
use FastForward\DevTools\Path\DevToolsPathResolver;
24+
use Symfony\Component\Filesystem\Path;
25+
26+
/**
27+
* Detects Composer global DevTools installations from known Composer home paths.
28+
*/
29+
final readonly class ComposerSelfUpdateScopeResolver implements SelfUpdateScopeResolverInterface
30+
{
31+
private const string PACKAGE_PATH = 'vendor/fast-forward/dev-tools';
32+
33+
/**
34+
* @param EnvironmentInterface $environment reads Composer home environment values
35+
* @param string|null $packagePath the DevTools package path; defaults to the active package root
36+
*/
37+
public function __construct(
38+
private EnvironmentInterface $environment,
39+
private ?string $packagePath = null,
40+
) {}
41+
42+
/**
43+
* Returns whether DevTools is running from Composer's global installation.
44+
*/
45+
public function isGlobalInstallation(): bool
46+
{
47+
$packagePath = Path::canonicalize($this->packagePath ?? DevToolsPathResolver::getPackagePath());
48+
49+
foreach ($this->getComposerHomeCandidates() as $composerHome) {
50+
$globalPackagePath = Path::canonicalize(Path::join($composerHome, self::PACKAGE_PATH));
51+
52+
if ($packagePath === $globalPackagePath || str_starts_with(
53+
$packagePath,
54+
$globalPackagePath . \DIRECTORY_SEPARATOR
55+
)) {
56+
return true;
57+
}
58+
}
59+
60+
return false;
61+
}
62+
63+
/**
64+
* Returns candidate Composer home directories for supported platforms.
65+
*
66+
* @return list<string>
67+
*/
68+
private function getComposerHomeCandidates(): array
69+
{
70+
$candidates = [];
71+
$composerHome = $this->environment->get('COMPOSER_HOME');
72+
73+
if (null !== $composerHome && '' !== $composerHome) {
74+
$candidates[] = $composerHome;
75+
}
76+
77+
$home = $this->environment->get('HOME');
78+
79+
if (null !== $home && '' !== $home) {
80+
$candidates[] = Path::join($home, '.composer');
81+
$candidates[] = Path::join($home, '.config/composer');
82+
$candidates[] = Path::join($home, 'Library/Application Support/Composer');
83+
}
84+
85+
$appData = $this->environment->get('APPDATA');
86+
87+
if (null !== $appData && '' !== $appData) {
88+
$candidates[] = Path::join($appData, 'Composer');
89+
}
90+
91+
return array_values(array_unique($candidates));
92+
}
93+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Fast Forward Development Tools for PHP projects.
7+
*
8+
* This file is part of fast-forward/dev-tools project.
9+
*
10+
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
11+
* @license https://opensource.org/licenses/MIT MIT License
12+
*
13+
* @see https://github.com/php-fast-forward/
14+
* @see https://github.com/php-fast-forward/dev-tools
15+
* @see https://github.com/php-fast-forward/dev-tools/issues
16+
* @see https://php-fast-forward.github.io/dev-tools/
17+
* @see https://datatracker.ietf.org/doc/html/rfc2119
18+
*/
19+
20+
namespace FastForward\DevTools\SelfUpdate;
21+
22+
/**
23+
* Resolves whether self-update SHOULD target the local project or Composer global project.
24+
*/
25+
interface SelfUpdateScopeResolverInterface
26+
{
27+
/**
28+
* Returns whether DevTools is running from Composer's global installation.
29+
*/
30+
public function isGlobalInstallation(): bool;
31+
}

src/ServiceProvider/DevToolsServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@
8686
use FastForward\DevTools\Process\ProcessQueueInterface;
8787
use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator;
8888
use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateRunner;
89+
use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateScopeResolver;
8990
use FastForward\DevTools\SelfUpdate\ComposerVersionChecker;
9091
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
92+
use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
9193
use FastForward\DevTools\SelfUpdate\VersionCheckerInterface;
9294
use FastForward\DevTools\SelfUpdate\VersionCheckNotifier;
9395
use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface;
@@ -147,6 +149,7 @@ public function getFactories(): array
147149

148150
// Self-update
149151
SelfUpdateRunnerInterface::class => get(ComposerSelfUpdateRunner::class),
152+
SelfUpdateScopeResolverInterface::class => get(ComposerSelfUpdateScopeResolver::class),
150153
VersionCheckerInterface::class => get(ComposerVersionChecker::class),
151154
VersionCheckNotifierInterface::class => get(VersionCheckNotifier::class),
152155
WorkingDirectorySwitcherInterface::class => get(WorkingDirectorySwitcher::class),

tests/Console/Command/SelfUpdateCommandTest.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
2525
use FastForward\DevTools\Reflection\ClassReflection;
2626
use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface;
27+
use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface;
2728
use PHPUnit\Framework\Attributes\CoversClass;
2829
use PHPUnit\Framework\Attributes\Test;
2930
use PHPUnit\Framework\Attributes\UsesClass;
@@ -53,6 +54,11 @@ final class SelfUpdateCommandTest extends TestCase
5354
*/
5455
private ObjectProphecy $logger;
5556

57+
/**
58+
* @var ObjectProphecy<SelfUpdateScopeResolverInterface>
59+
*/
60+
private ObjectProphecy $scopeResolver;
61+
5662
/**
5763
* @var ObjectProphecy<InputInterface>
5864
*/
@@ -71,6 +77,7 @@ final class SelfUpdateCommandTest extends TestCase
7177
protected function setUp(): void
7278
{
7379
$this->selfUpdateRunner = $this->prophesize(SelfUpdateRunnerInterface::class);
80+
$this->scopeResolver = $this->prophesize(SelfUpdateScopeResolverInterface::class);
7481
$this->logger = $this->prophesize(LoggerInterface::class);
7582
$this->input = $this->prophesize(InputInterface::class);
7683
$this->output = $this->prophesize(OutputInterface::class);
@@ -80,7 +87,11 @@ protected function setUp(): void
8087
->will(static function (): void {});
8188
$this->logger->error(Argument::cetera())
8289
->will(static function (): void {});
83-
$this->command = new SelfUpdateCommand($this->selfUpdateRunner->reveal(), $this->logger->reveal());
90+
$this->command = new SelfUpdateCommand(
91+
$this->selfUpdateRunner->reveal(),
92+
$this->scopeResolver->reveal(),
93+
$this->logger->reveal()
94+
);
8495
}
8596

8697
/**
@@ -101,7 +112,7 @@ public function getCommandNamesWillReturnAttributeNameAndAliases(): void
101112
#[Test]
102113
public function executeWillUpdateProjectInstallation(): void
103114
{
104-
$this->input->getOption('global')
115+
$this->scopeResolver->isGlobalInstallation()
105116
->willReturn(false);
106117
$this->selfUpdateRunner->update(false, $this->output->reveal())
107118
->willReturn(SelfUpdateCommand::SUCCESS)
@@ -116,15 +127,30 @@ public function executeWillUpdateProjectInstallation(): void
116127
#[Test]
117128
public function executeWillReturnFailureWhenUpdateFails(): void
118129
{
119-
$this->input->getOption('global')
120-
->willReturn(true);
121-
$this->selfUpdateRunner->update(true, $this->output->reveal())
130+
$this->scopeResolver->isGlobalInstallation()
131+
->willReturn(false);
132+
$this->selfUpdateRunner->update(false, $this->output->reveal())
122133
->willReturn(SelfUpdateCommand::FAILURE)
123134
->shouldBeCalledOnce();
124135

125136
self::assertSame(SelfUpdateCommand::FAILURE, $this->executeCommand());
126137
}
127138

139+
/**
140+
* @return void
141+
*/
142+
#[Test]
143+
public function executeWillUpdateGlobalInstallationWhenCurrentBinaryIsGlobal(): void
144+
{
145+
$this->scopeResolver->isGlobalInstallation()
146+
->willReturn(true);
147+
$this->selfUpdateRunner->update(true, $this->output->reveal())
148+
->willReturn(SelfUpdateCommand::SUCCESS)
149+
->shouldBeCalledOnce();
150+
151+
self::assertSame(SelfUpdateCommand::SUCCESS, $this->executeCommand());
152+
}
153+
128154
/**
129155
* @return int
130156
*/

0 commit comments

Comments
 (0)