From fe01d3e7971b36ed780072d2f39b75edaaf55d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 11 May 2026 17:16:37 -0300 Subject: [PATCH 1/5] fix: robustly resolve composer home for self-update scope --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 31597f3b6..7a7e28507 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 31597f3b686ee592b42ccc6d073cf89f26c5f072 +Subproject commit 7a7e28507f30615ee494efde03393924b6dfb04a From 090f944c94a39ddbdb2049a68d862d6dde44b984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 11 May 2026 17:18:13 -0300 Subject: [PATCH 2/5] fix(self-update): include XDG_CONFIG_HOME as composer home candidate --- .../ComposerSelfUpdateScopeResolver.php | 6 ++++ .../ComposerSelfUpdateScopeResolverTest.php | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php index f01855f9c..6bde23d64 100644 --- a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php +++ b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php @@ -74,6 +74,12 @@ private function getComposerHomeCandidates(): array $candidates[] = $composerHome; } + $xdgConfigHome = $this->environment->get('XDG_CONFIG_HOME'); + + if (null !== $xdgConfigHome && '' !== $xdgConfigHome) { + $candidates[] = Path::join($xdgConfigHome, 'composer'); + } + $home = $this->environment->get('HOME'); if (null !== $home && '' !== $home) { diff --git a/tests/SelfUpdate/ComposerSelfUpdateScopeResolverTest.php b/tests/SelfUpdate/ComposerSelfUpdateScopeResolverTest.php index 84c87d5f8..c2f599506 100644 --- a/tests/SelfUpdate/ComposerSelfUpdateScopeResolverTest.php +++ b/tests/SelfUpdate/ComposerSelfUpdateScopeResolverTest.php @@ -57,6 +57,8 @@ public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderComposerH ->willReturn(null); $this->environment->get('APPDATA') ->willReturn(null); + $this->environment->get('XDG_CONFIG_HOME') + ->willReturn(null); $resolver = new ComposerSelfUpdateScopeResolver( $this->environment->reveal(), '/home/felipe/.composer/vendor/fast-forward/dev-tools', @@ -77,6 +79,8 @@ public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderDefaultCo ->willReturn('/Users/felipe'); $this->environment->get('APPDATA') ->willReturn(null); + $this->environment->get('XDG_CONFIG_HOME') + ->willReturn(null); $resolver = new ComposerSelfUpdateScopeResolver( $this->environment->reveal(), '/Users/felipe/Library/Application Support/Composer/vendor/fast-forward/dev-tools', @@ -97,6 +101,8 @@ public function isGlobalInstallationWillReturnFalseWhenPackageLivesUnderProjectV ->willReturn('/home/felipe'); $this->environment->get('APPDATA') ->willReturn(null); + $this->environment->get('XDG_CONFIG_HOME') + ->willReturn(null); $resolver = new ComposerSelfUpdateScopeResolver( $this->environment->reveal(), '/home/felipe/project/vendor/fast-forward/dev-tools', @@ -104,4 +110,26 @@ public function isGlobalInstallationWillReturnFalseWhenPackageLivesUnderProjectV self::assertFalse($resolver->isGlobalInstallation()); } + + /** + * @return void + */ + #[Test] + public function isGlobalInstallationWillReturnTrueWhenPackageLivesUnderXdgComposerHome(): void + { + $this->environment->get('COMPOSER_HOME') + ->willReturn(null); + $this->environment->get('HOME') + ->willReturn(null); + $this->environment->get('APPDATA') + ->willReturn(null); + $this->environment->get('XDG_CONFIG_HOME') + ->willReturn('/tmp/xdg'); + $resolver = new ComposerSelfUpdateScopeResolver( + $this->environment->reveal(), + '/tmp/xdg/composer/vendor/fast-forward/dev-tools', + ); + + self::assertTrue($resolver->isGlobalInstallation()); + } } From 8c7ea576205f68c2721715f4dfd22dfe2513f56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 11 May 2026 17:22:49 -0300 Subject: [PATCH 3/5] fix(self-update): use realpath via Safe namespace for global scope checks --- .../ComposerSelfUpdateScopeResolver.php | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php index 6bde23d64..978659fb1 100644 --- a/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php +++ b/src/SelfUpdate/ComposerSelfUpdateScopeResolver.php @@ -22,6 +22,9 @@ use FastForward\DevTools\Environment\EnvironmentInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use Symfony\Component\Filesystem\Path; +use Throwable; + +use function Safe\realpath; /** * Detects Composer global DevTools installations from known Composer home paths. @@ -44,10 +47,10 @@ public function __construct( */ public function isGlobalInstallation(): bool { - $packagePath = Path::canonicalize($this->packagePath ?? DevToolsPathResolver::getPackagePath()); + $packagePath = $this->normalizePath($this->packagePath ?? DevToolsPathResolver::getPackagePath()); foreach ($this->getComposerHomeCandidates() as $composerHome) { - $globalPackagePath = Path::canonicalize(Path::join($composerHome, self::PACKAGE_PATH)); + $globalPackagePath = $this->normalizePath(Path::join($composerHome, self::PACKAGE_PATH)); if ($packagePath === $globalPackagePath || str_starts_with( $packagePath, @@ -96,4 +99,18 @@ private function getComposerHomeCandidates(): array return array_values(array_unique($candidates)); } + + /** + * Safely canonicalizes a path, resolving symlinks when available. + * + * @param string $path + */ + private function normalizePath(string $path): string + { + try { + return Path::canonicalize(realpath($path)); + } catch (Throwable) { + return Path::canonicalize($path); + } + } } From f4e63d8bdddb314707e2298f0cf41ea9d42f23c3 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 20:26:02 +0000 Subject: [PATCH 4/5] Update wiki submodule pointer for PR #337 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 7a7e28507..d9b4ec921 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 7a7e28507f30615ee494efde03393924b6dfb04a +Subproject commit d9b4ec9214202b274067d3e6deb18770447e97d1 From 1a3bef290bdeb55f4edfa89f157de91644a9a2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Mon, 11 May 2026 17:26:05 -0300 Subject: [PATCH 5/5] chore(changelog): document self-update scope detection fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8466270..8790fdee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- 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). + ## [1.25.2] - 2026-05-11 ### Fixed