Skip to content

Commit 7abc73d

Browse files
fix: improve global self-update scope detection (#337)
* fix: robustly resolve composer home for self-update scope * fix(self-update): include XDG_CONFIG_HOME as composer home candidate * fix(self-update): use realpath via Safe namespace for global scope checks * Update wiki submodule pointer for PR #337 * chore(changelog): document self-update scope detection fix --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 18d1cf4 commit 7abc73d

4 files changed

Lines changed: 58 additions & 3 deletions

File tree

.github/wiki

Submodule wiki updated from 31597f3 to d9b4ec9

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+
- Improve self-update global/local scope detection by normalizing Composer home candidates with realpath fallback handling and `XDG_CONFIG_HOME` support, to avoid global installs accidentally running as local updates in symlinked or alternate Composer home environments (#335).
13+
1014
## [1.25.2] - 2026-05-11
1115

1216
### Fixed

src/SelfUpdate/ComposerSelfUpdateScopeResolver.php

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
use FastForward\DevTools\Environment\EnvironmentInterface;
2323
use FastForward\DevTools\Path\DevToolsPathResolver;
2424
use Symfony\Component\Filesystem\Path;
25+
use Throwable;
26+
27+
use function Safe\realpath;
2528

2629
/**
2730
* Detects Composer global DevTools installations from known Composer home paths.
@@ -44,10 +47,10 @@ public function __construct(
4447
*/
4548
public function isGlobalInstallation(): bool
4649
{
47-
$packagePath = Path::canonicalize($this->packagePath ?? DevToolsPathResolver::getPackagePath());
50+
$packagePath = $this->normalizePath($this->packagePath ?? DevToolsPathResolver::getPackagePath());
4851

4952
foreach ($this->getComposerHomeCandidates() as $composerHome) {
50-
$globalPackagePath = Path::canonicalize(Path::join($composerHome, self::PACKAGE_PATH));
53+
$globalPackagePath = $this->normalizePath(Path::join($composerHome, self::PACKAGE_PATH));
5154

5255
if ($packagePath === $globalPackagePath || str_starts_with(
5356
$packagePath,
@@ -74,6 +77,12 @@ private function getComposerHomeCandidates(): array
7477
$candidates[] = $composerHome;
7578
}
7679

80+
$xdgConfigHome = $this->environment->get('XDG_CONFIG_HOME');
81+
82+
if (null !== $xdgConfigHome && '' !== $xdgConfigHome) {
83+
$candidates[] = Path::join($xdgConfigHome, 'composer');
84+
}
85+
7786
$home = $this->environment->get('HOME');
7887

7988
if (null !== $home && '' !== $home) {
@@ -90,4 +99,18 @@ private function getComposerHomeCandidates(): array
9099

91100
return array_values(array_unique($candidates));
92101
}
102+
103+
/**
104+
* Safely canonicalizes a path, resolving symlinks when available.
105+
*
106+
* @param string $path
107+
*/
108+
private function normalizePath(string $path): string
109+
{
110+
try {
111+
return Path::canonicalize(realpath($path));
112+
} catch (Throwable) {
113+
return Path::canonicalize($path);
114+
}
115+
}
93116
}

tests/SelfUpdate/ComposerSelfUpdateScopeResolverTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderComposerH
5757
->willReturn(null);
5858
$this->environment->get('APPDATA')
5959
->willReturn(null);
60+
$this->environment->get('XDG_CONFIG_HOME')
61+
->willReturn(null);
6062
$resolver = new ComposerSelfUpdateScopeResolver(
6163
$this->environment->reveal(),
6264
'/home/felipe/.composer/vendor/fast-forward/dev-tools',
@@ -77,6 +79,8 @@ public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderDefaultCo
7779
->willReturn('/Users/felipe');
7880
$this->environment->get('APPDATA')
7981
->willReturn(null);
82+
$this->environment->get('XDG_CONFIG_HOME')
83+
->willReturn(null);
8084
$resolver = new ComposerSelfUpdateScopeResolver(
8185
$this->environment->reveal(),
8286
'/Users/felipe/Library/Application Support/Composer/vendor/fast-forward/dev-tools',
@@ -97,11 +101,35 @@ public function isGlobalInstallationWillReturnFalseWhenPackageLivesUnderProjectV
97101
->willReturn('/home/felipe');
98102
$this->environment->get('APPDATA')
99103
->willReturn(null);
104+
$this->environment->get('XDG_CONFIG_HOME')
105+
->willReturn(null);
100106
$resolver = new ComposerSelfUpdateScopeResolver(
101107
$this->environment->reveal(),
102108
'/home/felipe/project/vendor/fast-forward/dev-tools',
103109
);
104110

105111
self::assertFalse($resolver->isGlobalInstallation());
106112
}
113+
114+
/**
115+
* @return void
116+
*/
117+
#[Test]
118+
public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderXdgComposerHome(): void
119+
{
120+
$this->environment->get('COMPOSER_HOME')
121+
->willReturn(null);
122+
$this->environment->get('HOME')
123+
->willReturn(null);
124+
$this->environment->get('APPDATA')
125+
->willReturn(null);
126+
$this->environment->get('XDG_CONFIG_HOME')
127+
->willReturn('/tmp/xdg');
128+
$resolver = new ComposerSelfUpdateScopeResolver(
129+
$this->environment->reveal(),
130+
'/tmp/xdg/composer/vendor/fast-forward/dev-tools',
131+
);
132+
133+
self::assertTrue($resolver->isGlobalInstallation());
134+
}
107135
}

0 commit comments

Comments
 (0)