diff --git a/src/StaticPHP/Command/Dev/PackLibCommand.php b/src/StaticPHP/Command/Dev/PackLibCommand.php new file mode 100644 index 00000000..6f69cb05 --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackLibCommand.php @@ -0,0 +1,33 @@ +addArgument('library', InputArgument::REQUIRED, 'The library will be compiled'); + $this->addOption('show-libc-ver', null, null); + } + + public function handle(): int + { + $library = $this->getArgument('library'); + $show_libc_ver = $this->getOption('show-libc-ver'); + + $installer = new PackageInstaller(['pack-mode' => true]); + $installer->addBuildPackage($library); + + $installer->run(); + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index e6561705..22404b6d 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -9,6 +9,7 @@ use StaticPHP\Command\BuildTargetCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; +use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; @@ -63,6 +64,7 @@ class ConsoleApplication extends Application new IsInstalledCommand(), new EnvCommand(), new LintConfigCommand(), + new PackLibCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 1cdd229e..8a96f85e 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -5,7 +5,13 @@ declare(strict_types=1); namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; +use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; +use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; @@ -160,6 +166,114 @@ class LibraryPackage extends Package } } + /** + * Register default stages if not already defined by attributes. + * This is called after all attributes have been loaded. + * + * @internal Called by PackageLoader after loading attributes + */ + public function registerDefaultStages(): void + { + if (!$this->hasStage('packPrebuilt')) { + $this->addStage('packPrebuilt', [$this, 'packPrebuilt']); + } + // counting files before build stage + } + + /** + * Pack the prebuilt library into an archive. + * + * @internal this function is intended to be called by the dev:pack-lib command only + */ + public function packPrebuilt(): void + { + $target_dir = WORKING_DIR . '/dist'; + $placeholder_file = BUILD_ROOT_PATH . '/.spc-extract-placeholder.json'; + + if (!ApplicationContext::has(DirDiff::class)) { + throw new SPCInternalException('pack-dirdiff context not found for packPrebuilt stage. You cannot call "packPrebuilt" function manually.'); + } + // check whether this library has correctly installed files + if (!$this->isInstalled()) { + throw new ValidationException("Cannot pack prebuilt library [{$this->getName()}] because it is not fully installed."); + } + // get after-build buildroot file list + $increase_files = ApplicationContext::get(DirDiff::class)->getIncrementFiles(true); + + FileSystem::createDir($target_dir); + + // before pack, check if the dependency tree contains lib-suggests + $libraries = DependencyResolver::resolve([$this], include_suggests: true); + foreach ($libraries as $lib) { + if (PackageConfig::get($lib, 'suggests', []) !== []) { + throw new ValidationException("The library {$lib} has lib-suggests, packing [{$this->name}] is not safe, abort !"); + } + } + + $origin_files = []; + + // get pack placehoder defines + $placehoder = get_pack_replace(); + + // patch pkg-config and la files with absolute path + foreach ($increase_files as $file) { + if (str_ends_with($file, '.pc') || str_ends_with($file, '.la')) { + $content = FileSystem::readFile(BUILD_ROOT_PATH . '/' . $file); + $origin_files[$file] = $content; + // replace relative paths with absolute paths + $content = str_replace( + array_keys($placehoder), + array_values($placehoder), + $content + ); + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + } + } + + // add .spc-extract-placeholder.json in BUILD_ROOT_PATH + file_put_contents($placeholder_file, json_encode(array_keys($origin_files), JSON_PRETTY_PRINT)); + $increase_files[] = '.spc-extract-placeholder.json'; + + // every file mapped with BUILD_ROOT_PATH + // get BUILD_ROOT_PATH last dir part + $buildroot_part = basename(BUILD_ROOT_PATH); + $increase_files = array_map(fn ($file) => $buildroot_part . '/' . $file, $increase_files); + // write list to packlib_files.txt + FileSystem::writeFile(WORKING_DIR . '/packlib_files.txt', implode("\n", $increase_files)); + // pack + $filename = match (SystemTarget::getTargetOS()) { + 'Windows' => '{name}-{arch}-{os}.tgz', + 'Darwin' => '{name}-{arch}-{os}.txz', + 'Linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', + }; + $replace = [ + '{name}' => $this->getName(), + '{arch}' => arch2gnu(php_uname('m')), + '{os}' => strtolower(PHP_OS_FAMILY), + '{libc}' => SystemTarget::getLibc() ?? 'default', + '{libcver}' => SystemTarget::getLibcVersion() ?? 'default', + ]; + // detect suffix, for proper tar option + $tar_option = $this->getTarOptionFromSuffix($filename); + $filename = str_replace(array_keys($replace), array_values($replace), $filename); + $filename = $target_dir . '/' . $filename; + f_passthru("tar {$tar_option} {$filename} -T " . WORKING_DIR . '/packlib_files.txt'); + logger()->info('Pack library ' . $this->getName() . ' to ' . $filename . ' complete.'); + + // remove temp files + unlink($placeholder_file); + + foreach ($origin_files as $file => $content) { + // restore original files + if (file_exists(BUILD_ROOT_PATH . '/' . $file)) { + FileSystem::writeFile(BUILD_ROOT_PATH . '/' . $file, $content); + } + } + + // remove dirdiff + ApplicationContext::set(DirDiff::class, null); + } + /** * Get static library files for current package and its dependencies. */ @@ -215,4 +329,30 @@ class LibraryPackage extends Package { return BUILD_BIN_PATH; } + + /** + * Get tar compress options from suffix + * + * @param string $name Package file name + * @return string Tar options for packaging libs + */ + private function getTarOptionFromSuffix(string $name): string + { + if (str_ends_with($name, '.tar')) { + return '-cf'; + } + if (str_ends_with($name, '.tar.gz') || str_ends_with($name, '.tgz')) { + return '-czf'; + } + if (str_ends_with($name, '.tar.bz2') || str_ends_with($name, '.tbz2')) { + return '-cjf'; + } + if (str_ends_with($name, '.tar.xz') || str_ends_with($name, '.txz')) { + return '-cJf'; + } + if (str_ends_with($name, '.tar.lz') || str_ends_with($name, '.tlz')) { + return '-c --lzma -f'; + } + return '-cf'; + } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index b9a3debb..a01d29fc 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -14,6 +14,7 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; @@ -71,6 +72,9 @@ class PackageInstaller if (!$package->hasStage('build')) { throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.'); } + if (($this->options['pack-mode'] ?? false) === true && !empty($this->build_packages)) { + throw new WrongUsageException("In 'pack-mode', only one package can be built at a time. Cannot add package '{$package->getName()}' to build list."); + } $this->build_packages[$package->getName()] = $package; return $this; } @@ -195,8 +199,16 @@ class PackageInstaller InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { + $dirdiff = new DirDiff(BUILD_ROOT_PATH, false); + ApplicationContext::set(DirDiff::class, $dirdiff); + } /** @var LibraryPackage $package */ $status = $builder->buildPackage($package, $this->isBuildPackage($package)); + + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { + $package->runStage('packPrebuilt'); + } } catch (\Throwable $e) { if ($interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);