Skip to content

Commit 112151f

Browse files
committed
Fix --php override for xback so DebugServer can spawn the override binary
CodeRabbit flagged that bin/xback's --php option replaced $command[0] with the resolved override path, but DebugServer::executeTargetScript() selects the local-PHP execution branch via a strict $command[0] === 'php' check and otherwise throws "Custom command must start with 'php' or be a Docker/Podman/Kubectl command". Even if the check were loosened, the cmd template hardcoded the literal 'php' token, so the override would never be exec'd anyway. Plumb the override through DebugServer's options as `phpBinary` instead: xback keeps $command[0] as the literal 'php', and DebugServer substitutes the option (escapeshellarg-quoted) into both the local-command and default-script command templates, defaulting to 'php' when unset. Adds testXbackPhpOption that runs `xback --php=<absolute php> -- php <fixture>` and asserts the run doesn't fall through to the error path and emits the expected xback JSON.
1 parent b50f9a8 commit 112151f

3 files changed

Lines changed: 68 additions & 7 deletions

File tree

bin/xback

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,17 @@ function outputBacktraceJson(string $script, ?string $breakpointStr, string $con
187187
// Detect Docker/Podman/Kubectl commands
188188
$isDockerCommand = \Koriym\XdebugMcp\ContainerHelper::isContainerCommand($command);
189189

190-
// Apply --php override to local commands whose first token is a PHP binary.
191-
if (
190+
// Decide whether the --php override should apply to the local command.
191+
// Keep $command[0] as the literal 'php' so DebugServer's strict-equality
192+
// check still selects the local-PHP execution branch; the actual override
193+
// path is plumbed through DebugServer's `phpBinary` option below.
194+
$applyPhpOverride = (
192195
!$isDockerCommand
193196
&& $phpOverride !== null
194197
&& $phpOverride !== ''
195198
&& !empty($command)
196199
&& \Koriym\XdebugMcp\XdebugRunner::isPhpBinary($command[0])
197-
) {
198-
$command[0] = $phpOverride;
199-
}
200+
);
200201

201202
// Parse breakpoint now that we know if it's a Docker command
202203
if ($breakpointStr !== null) {
@@ -248,6 +249,10 @@ function outputBacktraceJson(string $script, ?string $breakpointStr, string $con
248249
'maxSteps' => 1,
249250
];
250251

252+
if ($applyPhpOverride) {
253+
$options['phpBinary'] = $phpOverride;
254+
}
255+
251256
// Capture DebugServer output
252257
ob_start();
253258
$server = new DebugServer($targetScript, Constants::XDEBUG_DEBUG_PORT, null, $options, true);

src/DebugServer.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,14 @@ private function executeTargetScript(): void
354354
$xdebugFlag = XdebugFinder::getXdebugFlag();
355355
$xdebugPart = $xdebugFlag !== '' ? $xdebugFlag . ' ' : '';
356356

357+
// Allow callers (e.g. xback --php=...) to override the spawned
358+
// PHP binary while keeping $command[0] as the literal 'php'.
359+
$phpBinary = ($this->options['phpBinary'] ?? '') !== ''
360+
? (string) $this->options['phpBinary']
361+
: 'php';
362+
357363
$cmd = sprintf(
358-
'XDEBUG_SESSION=xdebug-mcp php %s'
364+
'XDEBUG_SESSION=xdebug-mcp %s %s'
359365
. '-dxdebug.mode=debug,trace '
360366
. '-dxdebug.start_with_request=yes '
361367
. '-dxdebug.client_host=127.0.0.1 '
@@ -372,6 +378,7 @@ private function executeTargetScript(): void
372378
. '-derror_log=/tmp/php.log '
373379
. '-dauto_prepend_file=%s '
374380
. '%s',
381+
escapeshellarg($phpBinary),
375382
$xdebugPart,
376383
$this->debugPort,
377384
escapeshellarg($prependFilter),
@@ -391,8 +398,13 @@ private function executeTargetScript(): void
391398
$xdebugFlag = XdebugFinder::getXdebugFlag();
392399
$xdebugPart = $xdebugFlag !== '' ? $xdebugFlag . ' ' : '';
393400

401+
// Honor an optional PHP-binary override.
402+
$phpBinary = ($this->options['phpBinary'] ?? '') !== ''
403+
? (string) $this->options['phpBinary']
404+
: 'php';
405+
394406
$cmd = sprintf(
395-
'XDEBUG_SESSION=xdebug-mcp php %s'
407+
'XDEBUG_SESSION=xdebug-mcp %s %s'
396408
. '-dxdebug.mode=debug,trace '
397409
. '-dxdebug.start_with_request=yes '
398410
. '-dxdebug.client_host=127.0.0.1 '
@@ -405,6 +417,7 @@ private function executeTargetScript(): void
405417
. '-dxdebug.connect_timeout_ms=5000 '
406418
. '-dauto_prepend_file=%s '
407419
. '%s',
420+
escapeshellarg($phpBinary),
408421
$xdebugPart,
409422
$this->debugPort,
410423
escapeshellarg($prependFilter),

tests/Integration/XdebugCommandsTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use function var_export;
3535

3636
use const JSON_THROW_ON_ERROR;
37+
use const PHP_BINARY;
3738

3839
class XdebugCommandsTest extends TestCase
3940
{
@@ -412,6 +413,48 @@ public function testXbackCwdOption(): void
412413
$this->assertArrayHasKey('$schema', $payload);
413414
}
414415

416+
public function testXbackPhpOption(): void
417+
{
418+
if (! XdebugFinder::isXdebugAvailable()) {
419+
$this->markTestSkipped('Xdebug not available');
420+
}
421+
422+
// Resolve the current PHP binary as the override target. Using PHP_BINARY
423+
// guarantees the path exists and is executable on the running system.
424+
$phpBinary = realpath(PHP_BINARY);
425+
if ($phpBinary === false || ! is_executable($phpBinary)) {
426+
$this->markTestSkipped('Cannot resolve PHP_BINARY for --php override test');
427+
}
428+
429+
$fixture = dirname(__DIR__) . '/fixtures/print_cwd.php';
430+
431+
// Run with --php pointing at an absolute PHP binary path. Without the
432+
// fix this fails with "Custom command must start with 'php' or be a
433+
// Docker/Podman/Kubectl command" because $command[0] becomes the
434+
// absolute path and DebugServer's strict-equality check rejects it.
435+
$command = sprintf(
436+
'cd %s && ./bin/xback --php=%s -- php %s 2>&1',
437+
escapeshellarg(dirname(__DIR__, 2)),
438+
escapeshellarg($phpBinary),
439+
escapeshellarg($fixture),
440+
);
441+
442+
$output = shell_exec($command);
443+
$this->assertNotNull($output);
444+
$this->assertStringNotContainsString(
445+
"must start with 'php'",
446+
$output,
447+
'--php override should not fall through to the DebugServer error path',
448+
);
449+
450+
$jsonStart = strrpos($output, '{"$schema"');
451+
$this->assertNotFalse($jsonStart, $output);
452+
453+
$payload = json_decode(substr($output, $jsonStart), true, 512, JSON_THROW_ON_ERROR);
454+
$this->assertIsArray($payload);
455+
$this->assertArrayHasKey('$schema', $payload);
456+
}
457+
415458
public function testAllCommandsAreExecutable(): void
416459
{
417460
$commands = [

0 commit comments

Comments
 (0)