Keep the embed build inside its own prefix (#119) --skip-tests

* fix(installer): keep the embed build inside its own prefix

The libphp.so installer reuses the configure options of the PHP already on
the machine so the private build matches it feature for feature. A
distribution build records where *it* installs, though, and those paths are
absolute. On Debian and Ubuntu the options carry --mandir=/usr/share/man,
--includedir=/usr/include, --libdir, --libexecdir, --sysconfdir and
--localstatedir. Inherited unchanged, they survive the --prefix rewrite, and
`make install` tries to write into system directories:

    Installing PHP CLI man page:      /usr/share/man/man1/
    cp: cannot create regular file '/usr/share/man/man1/#INST@3023727#': Permission denied
    make: *** [Makefile:265: install-cli] Error 1
    Fatal error: Unable to install libphp.so: Command failed: make install

So the build only completed for root, or inside a container where the user
owns /usr. Everyone else got a PHP compiled from scratch and then thrown
away at the last step.

Two further options escape the same way. --program-suffix=8.3 installs the
binaries as php8.3 and php-config8.3, but LibPhpInstaller::installedVersion(),
writePhpIni() and UnixPlatform::findPhpConfig() all look for the plain names,
so a build that got past `make install` still looked like it had produced
nothing. --cache-file points at the distribution's build directory
(/tmp/buildd/nonexistent/config.cache), a path that does not exist here and
whose answers were recorded for a different prefix.

Drop all of them. Autoconf derives every installation directory from --prefix
when it is absent, so the private build lands entirely under the requested
prefix: bin/php, lib/libphp.so, include/php and share/man/man1. Options that
name a *dependency* rather than a destination, such as --with-zlib=/usr and
--with-layout=GNU, are untouched; the built PHP still matches the one it was
derived from.

* fix(installer): offer an existing build before asking to make one

Running a build twice asked four questions to arrive at the answer it
already had:

    The current PHP installation does not provide libphp.so: /usr
    Build a private PHP embed library now? [Y/n]
    PHP version [8.5.10]:
    Install directory [/home/alganet/.typephp]:
    PHP 8.5.10 with libphp.so already exists in /home/alganet/.typephp; use it? [Y/n]

The reuse check sat at the bottom because it needs a version and a
directory, and those were read from the prompts. But both have defaults
that are known without asking anyone: the directory this installer always
proposes, and the PHP that is running it. That is the case being answered
here, so it can be offered first.

Ask it up front. Accepting now ends the exchange at one question, and the
`--prefix` rewrite no longer has to be reasoned about at all. The later
check stays for a directory or version typed by hand, and is skipped when
the answers repeat the defaults, so the same question is never asked twice.

ensure() also fetched the release list from php.net before looking for an
existing build, so the reuse path made a network round trip to learn the
URL of an archive it was not going to download. Move the fetch next to the
install that needs it.

Nothing recorded PHP_HOME anywhere but this process, and that has not
changed: a fresh run still has to rediscover the build. Being asked once
instead of four times is the part that is worth fixing separately.
master
Alexandre Gomes Gaigalas 3 weeks ago committed by GitHub
parent b831dfacf8
commit d941c23ccd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 45
      phpunit/src/Installer/PhpBuildConfigurationTest.php
  2. 44
      src/Installer/LibPhpInstaller.php
  3. 33
      src/Installer/PhpBuildConfiguration.php

@ -57,7 +57,6 @@ final class PhpBuildConfigurationTest extends TestCase
['--includedir=/usr/include', '--disable-all', '--with-zlib=/usr'],
$parsed
);
self::assertContains('--includedir=/usr/include', $options);
self::assertContains('--disable-all', $options);
self::assertContains('--with-zlib=/usr', $options);
self::assertNotContains('CFLAGS=-g', $options);
@ -78,6 +77,50 @@ final class PhpBuildConfigurationTest extends TestCase
self::assertContains('--enable-cli', $options);
}
public function testDeriveKeepsInstallationDirectoriesInsideThePrefix(): void
{
// A Debian/Ubuntu build records absolute installation directories; reusing
// them makes `make install` write to /usr/share/man and /usr/include, which
// an unprivileged user cannot do.
$options = PhpBuildConfiguration::derive(
"'--prefix=/usr' '--exec-prefix=/usr' '--includedir=/usr/include' " .
"'--mandir=/usr/share/man' '--infodir=/usr/share/info' '--sysconfdir=/etc' " .
"'--localstatedir=/var' '--libdir=\${prefix}/lib/php' " .
"'--libexecdir=/usr/lib/x86_64-linux-gnu' '--datadir=\${prefix}/share/php/8.5' " .
"'--with-layout=GNU' '--with-openssl'",
'/home/test/.typephp'
);
foreach ($options as $option) {
self::assertStringNotContainsString('/usr/', $option);
self::assertStringNotContainsString('${prefix}', $option);
}
self::assertSame(['--prefix=/home/test/.typephp'], array_values(array_filter(
$options,
static fn(string $option): bool => str_starts_with($option, '--prefix=')
)));
self::assertContains('--with-layout=GNU', $options);
self::assertContains('--with-openssl', $options);
}
public function testDeriveDropsProgramNameTransformsAndStaleConfigureCaches(): void
{
// --program-suffix would install bin/php8.5 instead of bin/php, which the
// installer and the platform probes both look for by its plain name.
$options = PhpBuildConfiguration::derive(
"'--program-suffix=8.5' '--program-prefix=x' '--program-transform-name=s,x,x,' " .
"'--config-cache' '--cache-file=/tmp/buildd/nonexistent/config.cache' '--enable-pcntl'",
'/home/test/.typephp'
);
self::assertNotContains('--program-suffix=8.5', $options);
self::assertNotContains('--program-prefix=x', $options);
self::assertNotContains('--program-transform-name=s,x,x,', $options);
self::assertNotContains('--config-cache', $options);
self::assertNotContains('--cache-file=/tmp/buildd/nonexistent/config.cache', $options);
self::assertContains('--enable-pcntl', $options);
}
public function testParseShellWordsRejectsIncompleteInput(): void
{
$this->expectException(\InvalidArgumentException::class);

@ -25,27 +25,49 @@ final class LibPhpInstaller
}
$this->console->write("The current PHP installation does not provide libphp.so: {$currentPhpDir}");
$home = getenv('HOME') ?: (string) ($_SERVER['HOME'] ?? '');
$defaultPrefix = rtrim($home, '/') . '/.typephp';
$defaultVersion = PHP_VERSION;
// A build left by an earlier run answers every question below, so offer
// it before asking any of them. Both defaults are known without asking:
// the directory this installer always proposes, and the running PHP.
if ($this->offerInstalled($defaultPrefix, $defaultVersion)) {
return $this->activate($defaultPrefix);
}
if (!$this->console->confirm('Build a private PHP embed library now?', true)) {
return null;
}
$defaultVersion = PHP_VERSION;
$version = $this->console->ask("PHP version [{$defaultVersion}]: ", $defaultVersion);
if (!preg_match('/^8\.[45]\.\d+$/', $version)) {
throw new \RuntimeException('Only stable PHP 8.4.x and 8.5.x versions are supported by the automatic installer');
}
$release = $this->release($version);
$home = getenv('HOME') ?: (string) ($_SERVER['HOME'] ?? '');
$defaultPrefix = rtrim($home, '/') . '/.typephp';
$prefix = $this->expandHome($this->console->ask("Install directory [{$defaultPrefix}]: ", $defaultPrefix), $home);
if ($this->hasLibPhp($prefix) && $this->installedVersion($prefix) === $version
&& $this->console->confirm("PHP {$version} with libphp.so already exists in {$prefix}; use it?", true)) {
putenv('PHP_HOME=' . $prefix);
$_ENV['PHP_HOME'] = $prefix;
return $prefix;
}
$this->install($release, $prefix);
// The offer above covered only the default directory and the running
// PHP; the answers just given may name another build.
if (($prefix !== $defaultPrefix || $version !== $defaultVersion)
&& $this->offerInstalled($prefix, $version)) {
return $this->activate($prefix);
}
// Reaching php.net is pointless until a build is known to be needed.
$this->install($this->release($version), $prefix);
return $this->activate($prefix);
}
private function offerInstalled(string $prefix, string $version): bool
{
return $this->hasLibPhp($prefix)
&& $this->installedVersion($prefix) === $version
&& $this->console->confirm("PHP {$version} with libphp.so already exists in {$prefix}; use it?", true);
}
/** Point this process, and the build it runs, at the selected installation. */
private function activate(string $prefix): string
{
putenv('PHP_HOME=' . $prefix);
$_ENV['PHP_HOME'] = $prefix;
return $prefix;

@ -81,6 +81,34 @@ final class PhpBuildConfiguration
return $options;
}
/**
* Autoconf installation directories, program name transforms and cache files.
*
* A distribution build points these outside its own --prefix (--mandir=/usr/share/man,
* --includedir=/usr/include) or renames the installed binaries (--program-suffix=8.3).
* Inheriting them makes `make install` write to system paths the user cannot own,
* and hides bin/php behind a versioned name. Autoconf derives every one of them
* from --prefix when it is absent, so dropping them keeps the private build
* entirely inside the requested prefix.
*
* The versioned name is the Debian and Ubuntu packaging scheme, not a PPA
* addition: php8.3-dev in noble-updates/main carries --program-suffix=8.3
* and --mandir=/usr/share/man, and ppa:ondrej/php repeats it per version.
*
* @var list<string>
*/
private const array PREFIX_DERIVED = [
'--exec-prefix', '--bindir', '--sbindir', '--libexecdir', '--sysconfdir',
'--sharedstatedir', '--localstatedir', '--runstatedir', '--libdir',
'--includedir', '--oldincludedir', '--datarootdir', '--datadir',
'--infodir', '--localedir', '--mandir', '--docdir', '--htmldir',
'--dvidir', '--pdfdir', '--psdir',
'--program-prefix', '--program-suffix', '--program-transform-name',
// A cache recorded for the distribution prefix answers the wrong questions
// here, and its path may not even exist on this machine.
'--cache-file', '--config-cache',
];
/**
* @param string|list<string> $configureOptions
* @return list<string>
@ -91,7 +119,10 @@ final class PhpBuildConfiguration
'--prefix', '--with-config-file-path', '--with-config-file-scan-dir',
'--enable-embed', '--enable-cli', '--disable-cli', '--with-libdir',
];
$drop = ['--with-apxs', '--with-apxs2', '--enable-fpm', '--with-fpm-systemd'];
$drop = [
'--with-apxs', '--with-apxs2', '--enable-fpm', '--with-fpm-systemd',
...self::PREFIX_DERIVED,
];
$result = [];
$options = is_string($configureOptions) ? self::parseShellWords($configureOptions) : $configureOptions;
foreach ($options as $option) {

Loading…
Cancel
Save