Skip to content

Fix custom PHP binary deleted when binary path is the app root#117

Open
simonhamp wants to merge 6 commits into
mainfrom
fix-custom-php-binary-prune
Open

Fix custom PHP binary deleted when binary path is the app root#117
simonhamp wants to merge 6 commits into
mainfrom
fix-custom-php-binary-prune

Conversation

@simonhamp
Copy link
Copy Markdown
Member

Summary

Fixes #115.

When NATIVEPHP_PHP_BINARY_PATH is set to a value that points at the project root (e.g. ./), native:build deletes the entire built app right before electron-builder packages it.

Root cause

In PrunesVendorDirectory::pruneVendorDirectory(), the configured binary package directory was interpolated naively into "app/{$binaryPackageDirectory}". When the value normalizes to a no-op (./, ., /, or a trailing /.), Path::join('/build', 'app/./') collapses to /build/app, so the prune step removes the whole app directory. The previous ! empty() guard did not catch this because strings like ./ are non-empty.

native:run was unaffected because it does not run the prune step.

Fix

Trim the configured path of surrounding dots, slashes, and whitespace, then skip removal entirely when nothing meaningful remains:

$binaryPackageDirectory = trim($this->binaryPackageDirectory(), "./\\ \t\n\r\0\x0B");
if ($binaryPackageDirectory !== '' && $filesystem->exists($this->buildPath("app/{$binaryPackageDirectory}"))) {
    $filesystem->remove($this->buildPath("app/{$binaryPackageDirectory}"));
}

Tests

Added tests/Build/PruneVendorDirectoryTest.php:

Verified the regression test fails against the old code and passes with the fix. Full suite (114 tests) passes and Pint is clean.

🤖 Generated with Claude Code

simonhamp and others added 2 commits May 29, 2026 15:38
Trim the configured binary package path before pruning so values that
normalize to a no-op (./, ., /, trailing /.) no longer collapse to the
app root and delete the entire built app.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@simonhamp
Copy link
Copy Markdown
Member Author

@bmckay959 would you mind trying this fix out when you get a chance?

@simonhamp simonhamp marked this pull request as ready for review May 29, 2026 07:00
| Tests
|--------------------------------------------------------------------------
*/
it('removes the custom php binary package directory', function () use ($buildPath, $command) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this test....why do we want the custom binary removed? Feels like this test is introducing the bug the opposite direction? Basically, we don't want to delete the directory where the NATIVEPHP_PHP_BINARY points....or am I missing something?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable confusion. The binary is moved to a location in the app bundle itself during build. So leaving it in the source directory would include it twice 👍🏻

@PeteBishwhip
Copy link
Copy Markdown
Member

LGTM. Happy to merge pending passing CI.


expect("$buildPath/app/php-bin")->not->toBeDirectory();
expect("$buildPath/app/index.php")->toBeFile();
})->after(fn () => putenv('NATIVEPHP_PHP_BINARY_PATH'));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we're not currently able to support the ->after() method here.

$command->pruneVendorDirectory();

expect("$buildPath/app/index.php")->toBeFile();
expect("$buildPath/app/bin/win/x64/php.exe")->toBeFile();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on @gwleuverink comment from above, do we actually want the /app/bin/win/x64/php.exe to be removed? So basically add the expect("$buildPath/app/bin")->not->toBeDirectory()?

I can confirm this solved the bug, but I believe this is now including ALL the PHP versions with the app this way(So my build goes from 127MB to 212MB). So, if the binary is getting copied to a 'special' place inside the electron app(?), then can we not have the /bin directory also removed?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I may have misinterpreted the test. I'll have a another look at it to confirm

@gwleuverink
Copy link
Copy Markdown
Collaborator

Dug into this a bit more. The PR fixes the crash, but Ben's spotted a second thing worth handling before we merge: the binaries are still ending up in the bundle, which is the 127 to 212MB jump he measured.

What's happening is php.js already extracts the one binary we need into build/php during the build, so the php-*.zip files left in the source tree are redundant copies. Right now the prune skips them when the path is ./, because we're removing the whole binary directory and there's no way to do that at the app root without taking the app with it. So the test ends up asserting they survive, when really we'd want them gone.

The wrinkle on my side is that we don't own bin/ outright, only {os}/{arch}/php-*.zip inside it, so I'd rather not remove the whole directory in case someone keeps other things there. Targeting just our archives covers both the size and that:

$binDir = $this->buildPath('app/'.$this->binaryPackageDirectory().'bin');
$filesystem->remove(glob("{$binDir}/*/*/php-*.zip"));

For ./ that resolves to app/bin/*/*/php-*.zip, which never collapses to the app root, so the trim/guard isn't needed anymore either. Tested it against a bin/ with our zips plus some unrelated files and only the zips go.

@simonhamp happy to push this onto the branch and tweak the test to match if you're good with it. Or we iterate the os/arch matrix instead of the glob if you'd prefer, though php-*.zip feels specific enough.

@bmckay959
Copy link
Copy Markdown

Thanks for digging in more...as I mentioned in my initial issue, I'm still pretty new on the native php side of things, so I haven't fully wrapped my arms around the build process/order/when things get injected.

This feels like the right approach to me.

@gwleuverink
Copy link
Copy Markdown
Collaborator

Wouldn't have spotted it without your keen eye! @bmckay959 🙏🏻

Here's my suggested fix for review #121

This targets only the archives we manage, at their fixed bin/{os}/{arch}/php-*.zip location, never the containing directory. so if you happen to keep other files in ./bin those won't be wiped.

@gwleuverink
Copy link
Copy Markdown
Collaborator

The failing tests seem unrelated to this PR. Looks like the PendingRequest contract has broken backwards compat in a minor release. (Only happens with L13 - prefer-stable)

Laravel 13.13.0 tightened the HTTP client's header casting to throw on non-scalar values. The X-NativePHP-Secret header is set from config('nativephp-internal.secret'), which is null when NATIVEPHP_SECRET is unset, so every request blew up with an InvalidArgumentException.

Casting to a string keeps null as an empty string, which the client accepts. This is what older Laravel versions did implicitly.
@SRWieZ
Copy link
Copy Markdown
Member

SRWieZ commented Jun 5, 2026

Does it still work with absolute directory path ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom PHP binary gets deleted if in root of project

5 participants