Skip to content

Commit 449a8ae

Browse files
committed
fix: suppress nested xdebug warnings
1 parent b4eb1cf commit 449a8ae

13 files changed

Lines changed: 663 additions & 2 deletions

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ composer dev-tools tests -- --coverage=.dev-tools/coverage
128128
- Keep color behavior explicit in command wrappers for tools with known flags
129129
instead of probing binaries dynamically; for example, Symfony/Composer-style
130130
tools can receive `--ansi`, while PHPUnit uses `--colors=always`.
131+
- Keep child-process environment policy centralized in `ProcessQueue`
132+
configurators, including disabling Xdebug for non-coverage subprocesses while
133+
preserving coverage drivers when PCOV is unavailable.
131134

132135
**Naming Conventions:**
133136

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Fixed
1616

1717
- Preserve color-friendly nested command environments, explicit Symfony Console ANSI flags, concise process section labels, and offline-safe PhpMetrics execution without restoring PTY (#239)
18+
- Disable Xdebug for queued child processes unless coverage requires it without PCOV, reducing repeated Composer Xdebug warnings in orchestrated commands (#239)
1819
- Keep the reports workflow permission warning loop shell-safe for paths containing backslashes (#244)
1920
- Keep required PHPUnit matrix checks reporting after workflow-managed `.github/wiki` pointer commits by running the pull-request test workflow without top-level path filters and aligning the packaged consumer test wrapper (#230)
2021
- Publish pending and per-version required PHPUnit statuses for workflow-dispatched test runs so wiki pointer commits do not wait for an all-matrix aggregate status (#230)

docs/internals/architecture.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ command list:
7676
* - ``FastForward\DevTools\Process\ProcessBuilderInterface``
7777
- ``ProcessBuilderInterface`` and ``ProcessQueueInterface`` build and
7878
execute subprocess pipelines, while process environment and output
79-
Symfony-style sections keep nested command output readable without PTY.
79+
Symfony-style sections keep nested command output readable without PTY
80+
and suppress unnecessary Xdebug overhead in child processes.
8081
* - ``Filesystem and metadata``
8182
- ``FilesystemInterface``, ``ComposerJsonInterface``, and
8283
``FileLocatorInterface`` resolve local files, project metadata, and

docs/running/specialized-commands.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ Important details:
275275
- human-readable runs keep nested command output grouped with concise local
276276
section boundaries, pass color-friendly environment variables to
277277
subprocesses, and forward explicit ANSI flags to Symfony Console tools;
278+
- queued subprocesses run with ``XDEBUG_MODE=off`` when Xdebug is loaded but
279+
the command does not need Xdebug for coverage, or when PCOV can provide
280+
coverage instead;
278281
- it is the reporting stage used by ``standards``.
279282

280283
``skills``

docs/troubleshooting.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,26 @@ Recovery:
156156
When calling lower-level tools directly, use their non-interactive flags and
157157
provide required values through environment variables or workflow inputs.
158158

159+
Repeated Composer Xdebug Warnings
160+
---------------------------------
161+
162+
Scope: local orchestration commands.
163+
164+
Symptoms:
165+
166+
- Composer repeatedly prints that it is operating slower than normal because
167+
Xdebug is enabled;
168+
- aggregate commands emit the warning once per nested Composer subprocess.
169+
170+
Behavior:
171+
172+
DevTools sets ``XDEBUG_MODE=off`` for queued child processes when Xdebug is
173+
loaded but the child command does not need Xdebug for coverage. Coverage runs
174+
keep Xdebug available when PCOV is not loaded, and use PCOV when it is
175+
available. A warning printed by the top-level ``composer dev-tools`` process
176+
can still appear before DevTools itself starts; run that command with
177+
``XDEBUG_MODE=off`` when Xdebug is not needed for the top-level process.
178+
159179
GitHub Actions Error Annotations
160180
--------------------------------
161181

src/Php/Extension.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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\Php;
21+
22+
/**
23+
* Checks PHP runtime extension availability through PHP's native runtime.
24+
*/
25+
final class Extension implements ExtensionInterface
26+
{
27+
/**
28+
* Determines whether a PHP extension is loaded in the current runtime.
29+
*
30+
* @param string $name the extension name
31+
*
32+
* @return bool true when the extension is loaded
33+
*/
34+
public function isLoaded(string $name): bool
35+
{
36+
return \extension_loaded($name);
37+
}
38+
}

src/Php/ExtensionInterface.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\Php;
21+
22+
/**
23+
* Checks PHP runtime extension availability without coupling callers to global functions.
24+
*/
25+
interface ExtensionInterface
26+
{
27+
/**
28+
* Determines whether a PHP extension is loaded in the current runtime.
29+
*
30+
* @param string $name the extension name
31+
*
32+
* @return bool true when the extension is loaded
33+
*/
34+
public function isLoaded(string $name): bool;
35+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\Process;
21+
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
use Symfony\Component\Process\Process;
24+
25+
/**
26+
* Applies multiple process environment configurators in a stable order.
27+
*/
28+
final readonly class CompositeProcessEnvironmentConfigurator implements ProcessEnvironmentConfiguratorInterface
29+
{
30+
/**
31+
* @param iterable<ProcessEnvironmentConfiguratorInterface> $configurators ordered environment configurators
32+
*/
33+
public function __construct(
34+
private iterable $configurators
35+
) {}
36+
37+
/**
38+
* Configures environment variables for a queued process.
39+
*
40+
* @param Process $process the queued process that will be started
41+
* @param OutputInterface $output the parent output used to infer console capabilities
42+
*/
43+
public function configure(Process $process, OutputInterface $output): void
44+
{
45+
foreach ($this->configurators as $configurator) {
46+
$configurator->configure($process, $output);
47+
}
48+
}
49+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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\Process;
21+
22+
use FastForward\DevTools\Environment\EnvironmentInterface;
23+
use FastForward\DevTools\Php\ExtensionInterface;
24+
use Symfony\Component\Console\Output\OutputInterface;
25+
use Symfony\Component\Process\Process;
26+
27+
use function Safe\preg_match;
28+
29+
/**
30+
* Disables Xdebug for child processes unless coverage still needs it.
31+
*/
32+
final readonly class XdebugDisablingProcessEnvironmentConfigurator implements ProcessEnvironmentConfiguratorInterface
33+
{
34+
/**
35+
* @var list<string>
36+
*/
37+
private const array COVERAGE_ARGUMENT_PATTERNS = [
38+
'--coverage',
39+
'--coverage-clover',
40+
'--coverage-cobertura',
41+
'--coverage-crap4j',
42+
'--coverage-html',
43+
'--coverage-php',
44+
'--coverage-text',
45+
'--coverage-xml',
46+
'--min-coverage',
47+
];
48+
49+
/**
50+
* @param EnvironmentInterface $environment reads parent process environment variables
51+
* @param ExtensionInterface $extension checks PHP extension availability
52+
*/
53+
public function __construct(
54+
private EnvironmentInterface $environment,
55+
private ExtensionInterface $extension,
56+
) {}
57+
58+
/**
59+
* Configures Xdebug-related environment variables for nested commands.
60+
*
61+
* @param Process $process the queued process that will be started
62+
* @param OutputInterface $output the parent output used to infer console capabilities
63+
*/
64+
public function configure(Process $process, OutputInterface $output): void
65+
{
66+
unset($output);
67+
68+
if (! $this->shouldDisableXdebug($process)) {
69+
return;
70+
}
71+
72+
$env = $process->getEnv();
73+
74+
if (\array_key_exists('XDEBUG_MODE', $env)) {
75+
return;
76+
}
77+
78+
$env['XDEBUG_MODE'] = 'off';
79+
$process->setEnv($env);
80+
}
81+
82+
/**
83+
* Determines whether Xdebug can be disabled for the child process.
84+
*
85+
* @param Process $process the queued process that will be started
86+
*
87+
* @return bool true when Xdebug should be disabled for the child process
88+
*/
89+
private function shouldDisableXdebug(Process $process): bool
90+
{
91+
if (! $this->extension->isLoaded('xdebug')) {
92+
return false;
93+
}
94+
95+
if ($this->isTruthyEnvironmentFlag('COMPOSER_ALLOW_XDEBUG')) {
96+
return false;
97+
}
98+
99+
if (null !== $this->environment->get('XDEBUG_MODE')) {
100+
return false;
101+
}
102+
103+
if (! $this->requiresCoverage($process)) {
104+
return true;
105+
}
106+
107+
return $this->extension->isLoaded('pcov');
108+
}
109+
110+
/**
111+
* Determines whether the child process command line requests coverage.
112+
*
113+
* @param Process $process the queued process that will be started
114+
*
115+
* @return bool true when coverage arguments are present
116+
*/
117+
private function requiresCoverage(Process $process): bool
118+
{
119+
$commandLine = $process->getCommandLine();
120+
121+
foreach (self::COVERAGE_ARGUMENT_PATTERNS as $argument) {
122+
if ($this->containsCommandLineArgument($commandLine, $argument)) {
123+
return true;
124+
}
125+
}
126+
127+
return false;
128+
}
129+
130+
/**
131+
* Determines whether a command line contains an exact long option.
132+
*
133+
* @param string $commandLine the shell-escaped command line
134+
* @param string $argument the long option to find
135+
*
136+
* @return bool true when the exact option is present
137+
*/
138+
private function containsCommandLineArgument(string $commandLine, string $argument): bool
139+
{
140+
return 1 === preg_match(
141+
\sprintf('/(?:^|[\\s\'"])%s(?:=|[\\s\'"]|$)/', preg_quote($argument, '/')),
142+
$commandLine
143+
);
144+
}
145+
146+
/**
147+
* Determines whether an environment flag is set to a truthy value.
148+
*
149+
* @param string $name the environment variable name
150+
*
151+
* @return bool true when the environment variable is truthy
152+
*/
153+
private function isTruthyEnvironmentFlag(string $name): bool
154+
{
155+
$value = $this->environment->get($name, '');
156+
157+
return null !== $value && '' !== $value && '0' !== $value;
158+
}
159+
}

src/ServiceProvider/DevToolsServiceProvider.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,18 @@
7171
use FastForward\DevTools\License\GeneratorInterface;
7272
use FastForward\DevTools\License\Resolver;
7373
use FastForward\DevTools\License\ResolverInterface;
74+
use FastForward\DevTools\Php\Extension;
75+
use FastForward\DevTools\Php\ExtensionInterface;
7476
use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader;
7577
use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface;
7678
use FastForward\DevTools\Process\ColorPreservingProcessEnvironmentConfigurator;
79+
use FastForward\DevTools\Process\CompositeProcessEnvironmentConfigurator;
7780
use FastForward\DevTools\Process\ProcessBuilder;
7881
use FastForward\DevTools\Process\ProcessBuilderInterface;
7982
use FastForward\DevTools\Process\ProcessEnvironmentConfiguratorInterface;
8083
use FastForward\DevTools\Process\ProcessQueue;
8184
use FastForward\DevTools\Process\ProcessQueueInterface;
85+
use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator;
8286
use FastForward\DevTools\Path\DevToolsPathResolver;
8387
use FastForward\DevTools\Path\WorkingProjectPathResolver;
8488
use FastForward\DevTools\Psr\Clock\SystemClock;
@@ -116,9 +120,14 @@ public function getFactories(): array
116120
return [
117121
// Process
118122
EnvironmentInterface::class => get(Environment::class),
123+
ExtensionInterface::class => get(Extension::class),
119124
OutputCapabilityDetectorInterface::class => get(OutputCapabilityDetector::class),
120125
ProcessBuilderInterface::class => get(ProcessBuilder::class),
121-
ProcessEnvironmentConfiguratorInterface::class => get(ColorPreservingProcessEnvironmentConfigurator::class),
126+
ProcessEnvironmentConfiguratorInterface::class => create(CompositeProcessEnvironmentConfigurator::class)
127+
->constructor([
128+
get(ColorPreservingProcessEnvironmentConfigurator::class),
129+
get(XdebugDisablingProcessEnvironmentConfigurator::class),
130+
]),
122131
ProcessQueueInterface::class => get(ProcessQueue::class),
123132

124133
// Filesystem

0 commit comments

Comments
 (0)