mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-17 20:34:51 +08:00
214 lines
7.5 KiB
PHP
214 lines
7.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace StaticPHP\Package;
|
|
|
|
use StaticPHP\Config\PackageConfig;
|
|
use StaticPHP\DI\ApplicationContext;
|
|
use StaticPHP\Exception\SPCException;
|
|
use StaticPHP\Exception\SPCInternalException;
|
|
use StaticPHP\Exception\WrongUsageException;
|
|
use StaticPHP\Runtime\Shell\Shell;
|
|
use StaticPHP\Runtime\SystemTarget;
|
|
use StaticPHP\Util\FileSystem;
|
|
use StaticPHP\Util\GlobalPathTrait;
|
|
use StaticPHP\Util\InteractiveTerm;
|
|
use StaticPHP\Util\System\LinuxUtil;
|
|
|
|
class PackageBuilder
|
|
{
|
|
use GlobalPathTrait;
|
|
|
|
/** @var int make jobs count */
|
|
public readonly int $concurrency;
|
|
|
|
/**
|
|
* @param array $options Builder options
|
|
*/
|
|
public function __construct(protected array $options = [])
|
|
{
|
|
ApplicationContext::set(PackageBuilder::class, $this);
|
|
|
|
$this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
|
|
}
|
|
|
|
public function buildPackage(Package $package, bool $force = false): int
|
|
{
|
|
// init build dirs
|
|
if (!$package instanceof LibraryPackage) {
|
|
throw new SPCInternalException('Please, never try to build non-library packages directly.');
|
|
}
|
|
FileSystem::createDir($package->getBuildRootPath());
|
|
FileSystem::createDir($package->getIncludeDir());
|
|
FileSystem::createDir($package->getBinDir());
|
|
FileSystem::createDir($package->getLibDir());
|
|
|
|
if (!$package->hasStage('build')) {
|
|
throw new WrongUsageException("Package '{$package->name}' does not have a current platform 'build' stage defined.");
|
|
}
|
|
|
|
// validate package should be built
|
|
if (!$force) {
|
|
return $package->isInstalled() ? SPC_STATUS_ALREADY_BUILT : $this->buildPackage($package, true);
|
|
}
|
|
// check source is ready
|
|
if ($package->getType() !== 'virtual-target' && !is_dir($package->getSourceDir())) {
|
|
throw new WrongUsageException("Source directory for package '{$package->name}' does not exist. Please fetch the source before building.");
|
|
}
|
|
Shell::passthruCallback(function () {
|
|
InteractiveTerm::advance();
|
|
});
|
|
|
|
try {
|
|
if ($package->getType() !== 'virtual-target') {
|
|
// patch before build
|
|
$package->emitPatchBeforeBuild();
|
|
}
|
|
|
|
// build
|
|
$package->runStage('build');
|
|
|
|
if ($package->getType() !== 'virtual-target') {
|
|
// install license
|
|
if (($license = PackageConfig::get($package->getName(), 'license')) !== null) {
|
|
$this->installLicense($package, $license);
|
|
}
|
|
}
|
|
} catch (SPCException $e) {
|
|
// Ensure package information is bound if not already
|
|
if ($e->getPackageInfo() === null) {
|
|
$e->bindPackageInfo([
|
|
'package_name' => $package->name,
|
|
'package_type' => $package->type,
|
|
'package_class' => get_class($package),
|
|
'file' => null,
|
|
'line' => null,
|
|
]);
|
|
}
|
|
throw $e;
|
|
}
|
|
return SPC_STATUS_BUILT;
|
|
}
|
|
|
|
public function getOptions(): array
|
|
{
|
|
return $this->options;
|
|
}
|
|
|
|
public function getOption(string $key, mixed $default = null): mixed
|
|
{
|
|
return $this->options[$key] ?? $default;
|
|
}
|
|
|
|
/**
|
|
* Deploy the binary file from src to dst.
|
|
*/
|
|
public function deployBinary(string $src, string $dst, bool $executable = true): string
|
|
{
|
|
logger()->debug("Deploying binary from {$src} to {$dst}");
|
|
|
|
// file must exists
|
|
if (!file_exists($src)) {
|
|
throw new SPCInternalException("Deploy failed. Cannot find file: {$src}");
|
|
}
|
|
// dst dir must exists
|
|
FileSystem::createDir(dirname($dst));
|
|
|
|
// ignore copy to self
|
|
if (realpath($src) !== realpath($dst)) {
|
|
FileSystem::copy($src, $dst);
|
|
if ($executable) {
|
|
chmod($dst, 0755);
|
|
}
|
|
}
|
|
|
|
// file exist
|
|
if (!file_exists($dst)) {
|
|
throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}");
|
|
}
|
|
|
|
if (!$this->getOption('no-strip') && SystemTarget::isUnix()) {
|
|
// extract debug info
|
|
$this->extractDebugInfo($dst);
|
|
// strip binary
|
|
$this->stripBinary($dst);
|
|
}
|
|
|
|
// UPX for linux
|
|
$upx_option = $this->getOption('with-upx-pack');
|
|
if ($upx_option && SystemTarget::getTargetOS() === 'Linux' && $executable) {
|
|
if ($this->getOption('no-strip')) {
|
|
logger()->warning('UPX compression is not recommended when --no-strip is enabled.');
|
|
}
|
|
logger()->info("Compressing {$dst} with UPX");
|
|
shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
|
|
} elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) {
|
|
logger()->info("Compressing {$dst} with UPX");
|
|
shell()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst));
|
|
}
|
|
|
|
return $dst;
|
|
}
|
|
|
|
/**
|
|
* Extract debug information from binary file.
|
|
*
|
|
* @param string $binary_path the path to the binary file, including executables, shared libraries, etc
|
|
*/
|
|
public function extractDebugInfo(string $binary_path): string
|
|
{
|
|
$target_dir = BUILD_ROOT_PATH . '/debug';
|
|
FileSystem::createDir($target_dir);
|
|
$basename = basename($binary_path);
|
|
$debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug');
|
|
if (SystemTarget::getTargetOS() === 'Darwin') {
|
|
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
|
|
} elseif (SystemTarget::getTargetOS() === 'Linux') {
|
|
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
|
|
shell()
|
|
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
|
|
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
|
|
} else {
|
|
shell()
|
|
->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}")
|
|
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
|
|
}
|
|
} else {
|
|
logger()->debug('extractDebugInfo is only supported on Linux and macOS');
|
|
return '';
|
|
}
|
|
return $debug_file;
|
|
}
|
|
|
|
/**
|
|
* Strip unneeded symbols from binary file.
|
|
*/
|
|
public function stripBinary(string $binary_path): void
|
|
{
|
|
shell()->exec(match (SystemTarget::getTargetOS()) {
|
|
'Darwin' => "strip -S {$binary_path}",
|
|
'Linux' => "strip --strip-unneeded {$binary_path}",
|
|
'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now
|
|
default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
|
|
});
|
|
}
|
|
|
|
private function installLicense(Package $package, array $license): void
|
|
{
|
|
$dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName();
|
|
FileSystem::createDir($dir);
|
|
if (is_assoc_array($license)) {
|
|
$license = [$license];
|
|
}
|
|
|
|
foreach ($license as $index => $item) {
|
|
if ($item['type'] === 'text') {
|
|
FileSystem::writeFile("{$dir}/{$index}.txt", $item['text']);
|
|
} elseif ($item['type'] === 'file') {
|
|
FileSystem::copy("{$package->getSourceDir()}/{$item['path']}", "{$dir}/{$index}.txt");
|
|
}
|
|
}
|
|
}
|
|
}
|