Skip to content

Commit daa87e1

Browse files
committed
Add DirDiff utility and enhance package build process
- Introduced DirDiff class for tracking directory file changes. - Updated ConsoleApplication to use addCommand for build targets. - Enhanced PackageBuilder with methods for deploying binaries and extracting debug info. - Improved package installation logic to support shared extensions. - Added readline extension with patching for static builds.
1 parent c38f174 commit daa87e1

File tree

12 files changed

+544
-11
lines changed

12 files changed

+544
-11
lines changed

bin/spc-alpine-docker

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
set -e
44

55
# This file is using docker to run commands
6-
SPC_DOCKER_VERSION=v6
6+
SPC_DOCKER_VERSION=v7
77

88
# Detect docker can run
99
if ! which docker >/dev/null; then
@@ -123,6 +123,7 @@ COPY ./composer.* /app/
123123
ADD ./bin /app/bin
124124
RUN composer install --no-dev
125125
ADD ./config /app/config
126+
ADD ./spc.registry.json /app/spc.registry.json
126127
RUN bin/spc doctor --auto-fix
127128
RUN bin/spc install-pkg upx
128129

src/Package/Extension/readline.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Package\Extension;
6+
7+
use StaticPHP\Attribute\Package\AfterStage;
8+
use StaticPHP\Attribute\Package\BeforeStage;
9+
use StaticPHP\Attribute\Package\Extension;
10+
use StaticPHP\Package\PackageInstaller;
11+
use StaticPHP\Toolchain\Interface\ToolchainInterface;
12+
use StaticPHP\Util\SourcePatcher;
13+
14+
#[Extension('readline')]
15+
class readline
16+
{
17+
#[BeforeStage('php', 'unix-make-cli')]
18+
public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
19+
{
20+
if ($toolchain->isStatic()) {
21+
$php_src = $installer->getBuildPackage('php')->getSourceDir();
22+
SourcePatcher::patchFile('musl_static_readline.patch', $php_src);
23+
}
24+
}
25+
26+
#[AfterStage('php', 'unix-make-cli')]
27+
public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
28+
{
29+
if ($toolchain->isStatic()) {
30+
$php_src = $installer->getBuildPackage('php')->getSourceDir();
31+
SourcePatcher::patchFile('musl_static_readline.patch', $php_src, true);
32+
}
33+
}
34+
}

src/Package/Library/imap.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Package\Library;
6+
7+
use StaticPHP\Attribute\Package\AfterStage;
8+
use StaticPHP\Attribute\Package\Library;
9+
use StaticPHP\Attribute\PatchDescription;
10+
use StaticPHP\Runtime\SystemTarget;
11+
use StaticPHP\Util\FileSystem;
12+
13+
#[Library('imap')]
14+
class imap
15+
{
16+
#[AfterStage('php', 'patch-embed-scripts')]
17+
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
18+
public function afterPatchScripts(): void
19+
{
20+
if (SystemTarget::getLibc() === 'glibc') {
21+
FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"');
22+
}
23+
}
24+
}

src/Package/Target/php.php

Lines changed: 242 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
use StaticPHP\Runtime\SystemTarget;
2727
use StaticPHP\Toolchain\Interface\ToolchainInterface;
2828
use StaticPHP\Toolchain\ToolchainManager;
29+
use StaticPHP\Util\DirDiff;
2930
use StaticPHP\Util\FileSystem;
3031
use StaticPHP\Util\InteractiveTerm;
3132
use StaticPHP\Util\SourcePatcher;
3233
use StaticPHP\Util\SPCConfigUtil;
34+
use StaticPHP\Util\System\UnixUtil;
3335
use StaticPHP\Util\V2CompatLayer;
3436
use Symfony\Component\Console\Input\InputArgument;
3537
use Symfony\Component\Console\Input\InputOption;
@@ -312,11 +314,17 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer)
312314
if ($installer->isBuildPackage('php-cli')) {
313315
$package->runStage('unix-make-cli');
314316
}
317+
if ($installer->isBuildPackage('php-cgi')) {
318+
$package->runStage('unix-make-cgi');
319+
}
315320
if ($installer->isBuildPackage('php-fpm')) {
316321
$package->runStage('unix-make-fpm');
317322
}
318-
if ($installer->isBuildPackage('php-cgi')) {
319-
$package->runStage('unix-make-cgi');
323+
if ($installer->isBuildPackage('php-micro')) {
324+
$package->runStage('unix-make-micro');
325+
}
326+
if ($installer->isBuildPackage('php-embed')) {
327+
$package->runStage('unix-make-embed');
320328
}
321329
}
322330

@@ -330,9 +338,103 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install
330338
->exec("make -j{$concurrency} cli");
331339
}
332340

341+
#[Stage('unix-make-cgi')]
342+
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
343+
{
344+
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
345+
$concurrency = $builder->concurrency;
346+
shell()->cd($package->getSourceDir())
347+
->setEnv($this->makeVars($installer))
348+
->exec("make -j{$concurrency} cgi");
349+
}
350+
351+
#[Stage('unix-make-fpm')]
352+
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
353+
{
354+
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
355+
$concurrency = $builder->concurrency;
356+
shell()->cd($package->getSourceDir())
357+
->setEnv($this->makeVars($installer))
358+
->exec("make -j{$concurrency} fpm");
359+
}
360+
361+
#[Stage('unix-make-micro')]
362+
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
363+
{
364+
$phar_patched = false;
365+
try {
366+
if ($installer->isPackageResolved('ext-phar')) {
367+
$phar_patched = true;
368+
SourcePatcher::patchMicroPhar(self::getPHPVersionID());
369+
}
370+
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro'));
371+
// apply --with-micro-fake-cli option
372+
$vars = $this->makeVars($installer);
373+
$vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : '';
374+
// build
375+
shell()->cd($package->getSourceDir())
376+
->setEnv($vars)
377+
->exec("make -j{$builder->concurrency} micro");
378+
} finally {
379+
if ($phar_patched) {
380+
SourcePatcher::unpatchMicroPhar();
381+
}
382+
}
383+
}
384+
385+
#[Stage('unix-make-embed')]
386+
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
387+
{
388+
$shared_exts = array_filter(
389+
$installer->getResolvedPackages(),
390+
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
391+
);
392+
$install_modules = $shared_exts ? 'install-modules' : '';
393+
394+
// detect changes in module path
395+
$diff = new DirDiff(BUILD_MODULES_PATH, true);
396+
397+
$root = BUILD_ROOT_PATH;
398+
shell()->cd($package->getSourceDir())
399+
->setEnv($this->makeVars($installer))
400+
->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile')
401+
->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs");
402+
403+
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared -------------
404+
405+
// process libphp.so for shared embed
406+
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
407+
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
408+
if (file_exists($libphp_so)) {
409+
// rename libphp.so if -release is set
410+
if (SystemTarget::getTargetOS() === 'Linux') {
411+
$this->processLibphpSoFile($libphp_so, $installer);
412+
}
413+
// deploy
414+
$builder->deployBinary($libphp_so, $libphp_so, false);
415+
}
416+
417+
// process shared extensions that built-with-php
418+
$increment_files = $diff->getChangedFiles();
419+
foreach ($increment_files as $increment_file) {
420+
$builder->deployBinary($increment_file, $libphp_so, false);
421+
}
422+
423+
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
424+
425+
// process libphp.a for static embed
426+
$ar = getenv('AR') ?: 'ar';
427+
$libphp_a = "{$package->getLibDir()}/libphp.a";
428+
shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}");
429+
UnixUtil::exportDynamicSymbols($libphp_a);
430+
431+
// deploy embed php scripts
432+
$package->runStage('patch-embed-scripts');
433+
}
434+
333435
#[BuildFor('Darwin')]
334436
#[BuildFor('Linux')]
335-
public function build(TargetPackage $package): void
437+
public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void
336438
{
337439
// virtual target, do nothing
338440
if ($package->getName() !== 'php') {
@@ -342,6 +444,68 @@ public function build(TargetPackage $package): void
342444
$package->runStage('unix-buildconf');
343445
$package->runStage('unix-configure');
344446
$package->runStage('unix-make');
447+
448+
// collect shared extensions
449+
/** @var PhpExtensionPackage[] $shared_extensions */
450+
$shared_extensions = array_filter(
451+
$installer->getResolvedPackages(PhpExtensionPackage::class),
452+
fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp()
453+
);
454+
if (!empty($shared_extensions)) {
455+
if ($toolchain->isStatic()) {
456+
throw new WrongUsageException(
457+
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
458+
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
459+
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
460+
);
461+
}
462+
FileSystem::createDir(BUILD_MODULES_PATH);
463+
464+
// backup
465+
FileSystem::backupFile(BUILD_BIN_PATH . '/php-config');
466+
FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
467+
468+
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"');
469+
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes');
470+
}
471+
472+
try {
473+
foreach ($shared_extensions as $extension) {
474+
logger()->info('Building shared extensions...');
475+
$extension->buildSharedExtension();
476+
}
477+
} finally {
478+
// restore php-config
479+
if (!empty($shared_extensions)) {
480+
FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config');
481+
FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
482+
}
483+
}
484+
}
485+
486+
/**
487+
* Patch phpize and php-config if needed
488+
*/
489+
#[Stage('patch-embed-scripts')]
490+
public function patchPhpScripts(): void
491+
{
492+
// patch phpize
493+
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
494+
logger()->debug('Patching phpize prefix');
495+
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
496+
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
497+
}
498+
// patch php-config
499+
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
500+
logger()->debug('Patching php-config prefix and libs order');
501+
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
502+
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
503+
// move mimalloc to the beginning of libs
504+
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
505+
// move lstdc++ to the end of libs
506+
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
507+
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
508+
}
345509
}
346510

347511
/**
@@ -381,6 +545,10 @@ private function makeStaticExtensionString(PackageInstaller $installer): string
381545
return $str;
382546
}
383547

548+
/**
549+
* Make environment variables for php make.
550+
* This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking.
551+
*/
384552
private function makeVars(PackageInstaller $installer): array
385553
{
386554
$config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages()));
@@ -394,4 +562,75 @@ private function makeVars(PackageInstaller $installer): array
394562
'EXTRA_LIBS' => $config['libs'],
395563
]);
396564
}
565+
566+
/**
567+
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
568+
*/
569+
private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void
570+
{
571+
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
572+
$libDir = BUILD_LIB_PATH;
573+
$modulesDir = BUILD_MODULES_PATH;
574+
$realLibName = 'libphp.so';
575+
$cwd = getcwd();
576+
577+
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
578+
$release = $matches[1];
579+
$realLibName = "libphp-{$release}.so";
580+
$libphpRelease = "{$libDir}/{$realLibName}";
581+
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
582+
rename($libphpSo, $libphpRelease);
583+
}
584+
if (file_exists($libphpRelease)) {
585+
chdir($libDir);
586+
if (file_exists($libphpSo)) {
587+
unlink($libphpSo);
588+
}
589+
symlink($realLibName, 'libphp.so');
590+
shell()->exec(sprintf(
591+
'patchelf --set-soname %s %s',
592+
escapeshellarg($realLibName),
593+
escapeshellarg($libphpRelease)
594+
));
595+
}
596+
if (is_dir($modulesDir)) {
597+
chdir($modulesDir);
598+
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
599+
if (!$ext->isBuildShared()) {
600+
continue;
601+
}
602+
$name = $ext->getName();
603+
$versioned = "{$name}-{$release}.so";
604+
$unversioned = "{$name}.so";
605+
$src = "{$modulesDir}/{$versioned}";
606+
$dst = "{$modulesDir}/{$unversioned}";
607+
if (is_file($src)) {
608+
rename($src, $dst);
609+
shell()->exec(sprintf(
610+
'patchelf --set-soname %s %s',
611+
escapeshellarg($unversioned),
612+
escapeshellarg($dst)
613+
));
614+
}
615+
}
616+
}
617+
chdir($cwd);
618+
}
619+
620+
$target = "{$libDir}/{$realLibName}";
621+
if (file_exists($target)) {
622+
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
623+
$output = implode("\n", $output);
624+
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
625+
$currentSoname = $sonameMatch[1];
626+
if ($currentSoname !== basename($target)) {
627+
shell()->exec(sprintf(
628+
'patchelf --set-soname %s %s',
629+
escapeshellarg(basename($target)),
630+
escapeshellarg($target)
631+
));
632+
}
633+
}
634+
}
635+
}
397636
}

src/StaticPHP/ConsoleApplication.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ public function __construct()
3535
// only add target that contains artifact.source
3636
if ($package->hasStage('build')) {
3737
logger()->debug("Registering build target command for package: {$name}");
38-
$this->add(new BuildTargetCommand($name));
38+
$this->addCommand(new BuildTargetCommand($name));
3939
}
4040
}
4141

42+
// add core commands
4243
$this->addCommands([
4344
new DownloadCommand(),
4445
new DoctorCommand(),

0 commit comments

Comments
 (0)