diff --git a/.gitignore b/.gitignore index 776dbffd..269a267e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ docker/source/ # default source build root directory /buildroot/ +# default package root directory +/pkgroot/ + # tools cache files .php-cs-fixer.cache .phpunit.result.cache diff --git a/config/pkg.json b/config/pkg.json new file mode 100644 index 00000000..a7d1d0c8 --- /dev/null +++ b/config/pkg.json @@ -0,0 +1,32 @@ +{ + "upx-x86_64-linux": { + "type": "url", + "url": "https://github.com/upx/upx/releases/download/v4.2.2/upx-4.2.2-amd64_linux.tar.xz", + "extract-files": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "upx-aarch64-linux": { + "type": "url", + "url": "https://github.com/upx/upx/releases/download/v4.2.2/upx-4.2.2-arm64_linux.tar.xz", + "extract-files": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "nasm-x86_64-win": { + "type": "url", + "url": "https://www.nasm.us/pub/nasm/releasebuilds/2.16.01/win64/nasm-2.16.01-win64.zip", + "extract-files": { + "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + }, + "strawberry-perl-x86_64-win": { + "type": "url", + "url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip" + }, + "musl-toolchain-x86_64-linux": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz" + } +} \ No newline at end of file diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 0c49d8ca..355d5932 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -6,6 +6,7 @@ namespace SPC; use SPC\command\BuildCliCommand; use SPC\command\BuildLibsCommand; +use SPC\command\DeleteDownloadCommand; use SPC\command\dev\AllExtCommand; use SPC\command\dev\PhpVerCommand; use SPC\command\dev\SortConfigCommand; @@ -13,6 +14,7 @@ use SPC\command\DoctorCommand; use SPC\command\DownloadCommand; use SPC\command\DumpLicenseCommand; use SPC\command\ExtractCommand; +use SPC\command\InstallPkgCommand; use SPC\command\MicroCombineCommand; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\HelpCommand; @@ -23,7 +25,7 @@ use Symfony\Component\Console\Command\ListCommand; */ final class ConsoleApplication extends Application { - public const VERSION = '2.1.0-beta.3'; + public const VERSION = '2.1.0-beta.4'; public function __construct() { @@ -35,6 +37,8 @@ final class ConsoleApplication extends Application new BuildLibsCommand(), new DoctorCommand(), new DownloadCommand(), + new InstallPkgCommand(), + new DeleteDownloadCommand(), new DumpLicenseCommand(), new ExtractCommand(), new MicroCombineCommand(), diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index 2314a7f2..ee4d9d61 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -9,7 +9,7 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; -use SPC\store\SourceExtractor; +use SPC\store\SourceManager; use SPC\util\CustomExt; abstract class BuilderBase @@ -144,15 +144,15 @@ abstract class BuilderBase { CustomExt::loadCustomExt(); $this->emitPatchPoint('before-php-extract'); - SourceExtractor::initSource(sources: ['php-src']); + SourceManager::initSource(sources: ['php-src']); $this->emitPatchPoint('after-php-extract'); if ($this->getPHPVersionID() >= 80000) { $this->emitPatchPoint('before-micro-extract'); - SourceExtractor::initSource(sources: ['micro']); + SourceManager::initSource(sources: ['micro']); $this->emitPatchPoint('after-micro-extract'); } $this->emitPatchPoint('before-exts-extract'); - SourceExtractor::initSource(exts: $extensions); + SourceManager::initSource(exts: $extensions); $this->emitPatchPoint('after-exts-extract'); foreach ($extensions as $extension) { $class = CustomExt::getExtClass($extension); diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 583ad812..934ddaad 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -11,7 +11,7 @@ use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; -use SPC\store\SourceExtractor; +use SPC\store\SourceManager; use SPC\util\DependencyUtil; abstract class UnixBuilderBase extends BuilderBase @@ -134,7 +134,7 @@ abstract class UnixBuilderBase extends BuilderBase $this->emitPatchPoint('before-libs-extract'); // extract sources - SourceExtractor::initSource(libs: $sorted_libraries); + SourceManager::initSource(libs: $sorted_libraries); $this->emitPatchPoint('after-libs-extract'); diff --git a/src/SPC/builder/windows/WindowsBuilder.php b/src/SPC/builder/windows/WindowsBuilder.php index b007e7ff..38bfd783 100644 --- a/src/SPC/builder/windows/WindowsBuilder.php +++ b/src/SPC/builder/windows/WindowsBuilder.php @@ -10,7 +10,7 @@ use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; -use SPC\store\SourceExtractor; +use SPC\store\SourceManager; use SPC\store\SourcePatcher; use SPC\util\DependencyUtil; @@ -211,7 +211,7 @@ class WindowsBuilder extends BuilderBase } // extract sources - SourceExtractor::initSource(libs: $sorted_libraries); + SourceManager::initSource(libs: $sorted_libraries); // build all libs foreach ($this->libs as $lib) { diff --git a/src/SPC/builder/windows/library/openssl.php b/src/SPC/builder/windows/library/openssl.php index 18b98438..74a3b510 100644 --- a/src/SPC/builder/windows/library/openssl.php +++ b/src/SPC/builder/windows/library/openssl.php @@ -14,7 +14,8 @@ class openssl extends WindowsLibraryBase protected function build(): void { - $perl = file_exists(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe') ? (BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe') : SystemUtil::findCommand('perl.exe'); + $perl_path_native = PKG_ROOT_PATH . '\strawberry-perl-' . arch2gnu(php_uname('m')) . '-win\perl\bin\perl.exe'; + $perl = file_exists($perl_path_native) ? ($perl_path_native) : SystemUtil::findCommand('perl.exe'); if ($perl === null) { throw new RuntimeException('You need to install perl first! (easiest way is using static-php-cli command "doctor")'); } diff --git a/src/SPC/command/DeleteDownloadCommand.php b/src/SPC/command/DeleteDownloadCommand.php new file mode 100644 index 00000000..a953fbe6 --- /dev/null +++ b/src/SPC/command/DeleteDownloadCommand.php @@ -0,0 +1,86 @@ +addArgument('sources', InputArgument::REQUIRED, 'The sources/packages will be deleted, comma separated'); + $this->addOption('all', 'A', null, 'Delete all downloaded and locked sources/packages'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + if ($input->getOption('all')) { + $input->setArgument('sources', ''); + } + parent::initialize($input, $output); + } + + /** + * @throws FileSystemException + */ + public function handle(): int + { + try { + // get source list that will be downloaded + $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); + if (empty($sources)) { + logger()->notice('Removing downloads/ directory ...'); + FileSystem::removeDir(DOWNLOAD_PATH); + logger()->info('Removed downloads/ dir!'); + return static::SUCCESS; + } + $chosen_sources = $sources; + $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? []; + + foreach ($chosen_sources as $source) { + $source = trim($source); + if (!isset($lock[$source])) { + logger()->warning("Source/Package [{$source}] not locked or not downloaded, skipped."); + continue; + } + // remove download file/dir if exists + if ($lock[$source]['source_type'] === 'archive') { + if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$source]['filename']))) { + logger()->info('Deleting file ' . $path); + unlink($path); + } else { + logger()->warning("Source/Package [{$source}] file not found, skip deleting file."); + } + } else { + if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$source]['dirname']))) { + logger()->info('Deleting dir ' . $path); + FileSystem::removeDir($path); + } else { + logger()->warning("Source/Package [{$source}] directory not found, skip deleting dir."); + } + } + // remove locked sources + unset($lock[$source]); + } + FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + logger()->info('Delete success!'); + return static::SUCCESS; + } catch (DownloaderException $e) { + logger()->error($e->getMessage()); + return static::FAILURE; + } catch (WrongUsageException $e) { + logger()->critical($e->getMessage()); + return static::FAILURE; + } + } +} diff --git a/src/SPC/command/ExtractCommand.php b/src/SPC/command/ExtractCommand.php index 84768866..aacf5039 100644 --- a/src/SPC/command/ExtractCommand.php +++ b/src/SPC/command/ExtractCommand.php @@ -8,11 +8,11 @@ use SPC\builder\traits\UnixSystemUtilTrait; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; -use SPC\store\SourceExtractor; +use SPC\store\SourceManager; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; -#[AsCommand('extract', 'Extract required sources')] +#[AsCommand('extract', 'Extract required sources', ['extract-source'])] class ExtractCommand extends BaseCommand { use UnixSystemUtilTrait; @@ -34,7 +34,7 @@ class ExtractCommand extends BaseCommand $this->output->writeln('sources cannot be empty, at least contain one !'); return static::FAILURE; } - SourceExtractor::initSource(sources: $sources); + SourceManager::initSource(sources: $sources); logger()->info('Extract done !'); return static::SUCCESS; } diff --git a/src/SPC/command/InstallPkgCommand.php b/src/SPC/command/InstallPkgCommand.php new file mode 100644 index 00000000..b1ea3cd5 --- /dev/null +++ b/src/SPC/command/InstallPkgCommand.php @@ -0,0 +1,86 @@ +addArgument('packages', InputArgument::REQUIRED, 'The packages will be installed, comma separated'); + $this->addOption('shallow-clone', null, null, 'Clone shallow'); + $this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"'); + } + + /** + * @throws FileSystemException + */ + public function handle(): int + { + try { + // Use shallow-clone can reduce git resource download + if ($this->getOption('shallow-clone')) { + define('GIT_SHALLOW_CLONE', true); + } + + // Process -U options + $custom_urls = []; + foreach ($this->input->getOption('custom-url') as $value) { + [$pkg_name, $url] = explode(':', $value, 2); + $custom_urls[$pkg_name] = $url; + } + + $chosen_pkgs = array_map('trim', array_filter(explode(',', $this->getArgument('packages')))); + + // Download them + f_mkdir(DOWNLOAD_PATH); + $ni = 0; + $cnt = count($chosen_pkgs); + + foreach ($chosen_pkgs as $pkg) { + ++$ni; + if (isset($custom_urls[$pkg])) { + $config = Config::getPkg($pkg); + $new_config = [ + 'type' => 'url', + 'url' => $custom_urls[$pkg], + ]; + if (isset($config['extract'])) { + $new_config['extract'] = $config['extract']; + } + if (isset($config['filename'])) { + $new_config['filename'] = $config['filename']; + } + logger()->info("Installing source {$pkg} from custom url [{$ni}/{$cnt}]"); + PackageManager::installPackage($pkg, $new_config); + } else { + logger()->info("Fetching package {$pkg} [{$ni}/{$cnt}]"); + PackageManager::installPackage($pkg, Config::getPkg($pkg)); + } + } + $time = round(microtime(true) - START_TIME, 3); + logger()->info('Install packages complete, used ' . $time . ' s !'); + return static::SUCCESS; + } catch (DownloaderException $e) { + logger()->error($e->getMessage()); + return static::FAILURE; + } catch (WrongUsageException $e) { + logger()->critical($e->getMessage()); + return static::FAILURE; + } + } +} diff --git a/src/SPC/doctor/item/LinuxMuslCheck.php b/src/SPC/doctor/item/LinuxMuslCheck.php index c9639ff4..e672ca20 100644 --- a/src/SPC/doctor/item/LinuxMuslCheck.php +++ b/src/SPC/doctor/item/LinuxMuslCheck.php @@ -14,6 +14,7 @@ use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Downloader; use SPC\store\FileSystem; +use SPC\store\PackageManager; class LinuxMuslCheck { @@ -84,7 +85,6 @@ class LinuxMuslCheck /** @noinspection PhpUnused */ /** - * @throws DownloaderException * @throws FileSystemException * @throws WrongUsageException */ @@ -98,15 +98,10 @@ class LinuxMuslCheck logger()->warning('Current user is not root, using sudo for running command'); } $arch = arch2gnu(php_uname('m')); - $musl_compile_source = [ - 'type' => 'url', - 'url' => "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/{$arch}-musl-toolchain.tgz", - ]; - logger()->info('Downloading ' . $musl_compile_source['url']); - Downloader::downloadSource('musl-compile', $musl_compile_source); - logger()->info('Extracting musl-cross'); - FileSystem::extractSource('musl-compile', DOWNLOAD_PATH . "/{$arch}-musl-toolchain.tgz"); - shell()->exec($prefix . 'cp -rf ' . SOURCE_PATH . '/musl-compile/* /usr/local/musl'); + PackageManager::installPackage("musl-toolchain-{$arch}-linux"); + $pkg_root = PKG_ROOT_PATH . "/musl-toolchain-{$arch}-linux"; + shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); + FileSystem::removeDir($pkg_root); return true; } catch (RuntimeException) { return false; diff --git a/src/SPC/doctor/item/WindowsToolCheckList.php b/src/SPC/doctor/item/WindowsToolCheckList.php index 0115c3da..df4eb050 100644 --- a/src/SPC/doctor/item/WindowsToolCheckList.php +++ b/src/SPC/doctor/item/WindowsToolCheckList.php @@ -9,8 +9,8 @@ use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; use SPC\doctor\CheckResult; use SPC\exception\RuntimeException; -use SPC\store\Downloader; use SPC\store\FileSystem; +use SPC\store\PackageManager; class WindowsToolCheckList { @@ -64,8 +64,9 @@ class WindowsToolCheckList #[AsCheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)] public function checkPerl(): ?CheckResult { - if (file_exists(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe')) { - return CheckResult::ok(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe'); + $arch = arch2gnu(php_uname('m')); + if (file_exists(PKG_ROOT_PATH . '\strawberry-perl-' . $arch . '-win\perl\bin\perl.exe')) { + return CheckResult::ok(PKG_ROOT_PATH . '\strawberry-perl-' . $arch . '-win\perl\bin\perl.exe'); } if (($path = SystemUtil::findCommand('perl.exe')) === null) { return CheckResult::fail('perl not found in path.', 'install-perl'); @@ -91,32 +92,15 @@ class WindowsToolCheckList #[AsFixItem('install-nasm')] public function installNasm(): bool { - // The hardcoded version here is to be consistent with the version compiled by `musl-cross-toolchain`. - $nasm_ver = '2.16.01'; - $nasm_dist = "nasm-{$nasm_ver}"; - $source = [ - 'type' => 'url', - 'url' => "https://www.nasm.us/pub/nasm/releasebuilds/{$nasm_ver}/win64/{$nasm_dist}-win64.zip", - ]; - logger()->info('Downloading ' . $source['url']); - Downloader::downloadSource('nasm', $source); - FileSystem::extractSource('nasm', DOWNLOAD_PATH . "\\{$nasm_dist}-win64.zip"); - copy(SOURCE_PATH . "\\nasm\\{$nasm_dist}\\nasm.exe", PHP_SDK_PATH . '\bin\nasm.exe'); - copy(SOURCE_PATH . "\\nasm\\{$nasm_dist}\\ndisasm.exe", PHP_SDK_PATH . '\bin\ndisasm.exe'); + PackageManager::installPackage('nasm-x86_64-win'); return true; } #[AsFixItem('install-perl')] public function installPerl(): bool { - $url = 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip'; - $source = [ - 'type' => 'url', - 'url' => $url, - ]; - logger()->info("Downloading {$url}"); - Downloader::downloadSource('strawberry-perl', $source); - FileSystem::extractSource('strawberry-perl', DOWNLOAD_PATH . '\strawberry-perl-5.38.0.1-64bit-portable.zip', '../buildroot/perl'); + $arch = arch2gnu(php_uname('m')); + PackageManager::installPackage("strawberry-perl-{$arch}-win"); return true; } } diff --git a/src/SPC/store/Config.php b/src/SPC/store/Config.php index 01328be2..65340729 100644 --- a/src/SPC/store/Config.php +++ b/src/SPC/store/Config.php @@ -12,6 +12,8 @@ use SPC\exception\WrongUsageException; */ class Config { + public static ?array $pkg = null; + public static ?array $source = null; public static ?array $lib = null; @@ -31,6 +33,19 @@ class Config return self::$source[$name] ?? null; } + /** + * Read pkg from pkg.json + * + * @throws FileSystemException + */ + public static function getPkg(string $name): ?array + { + if (self::$pkg === null) { + self::$pkg = FileSystem::loadConfigArray('pkg'); + } + return self::$pkg[$name] ?? null; + } + /** * 根据不同的操作系统分别选择不同的 lib 库依赖项 * 如果 key 为 null,那么直接返回整个 meta。 diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index 2cb0fa37..16e26b2a 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -246,6 +246,92 @@ class Downloader }*/ } + public static function downloadPackage(string $name, ?array $pkg = null, bool $force = false): void + { + if ($pkg === null) { + $pkg = Config::getPkg($name); + } + + if ($pkg === null) { + logger()->warning('Package {name} unknown. Skipping.', ['name' => $name]); + return; + } + + if (!is_dir(DOWNLOAD_PATH)) { + FileSystem::createDir(DOWNLOAD_PATH); + } + + // load lock file + if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) { + $lock = []; + } else { + $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? []; + } + // If lock file exists, skip downloading + if (isset($lock[$name]) && !$force) { + if ($lock[$name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename'])) { + logger()->notice("Package [{$name}] already downloaded: " . $lock[$name]['filename']); + return; + } + if ($lock[$name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])) { + logger()->notice("Package [{$name}] already downloaded: " . $lock[$name]['dirname']); + return; + } + } + + try { + switch ($pkg['type']) { + case 'bitbuckettag': // BitBucket Tag + [$url, $filename] = self::getLatestBitbucketTag($name, $pkg); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'ghtar': // GitHub Release (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $pkg); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'ghtagtar': // GitHub Tag (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags'); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'ghrel': // GitHub Release (uploaded) + [$url, $filename] = self::getLatestGithubRelease($name, $pkg); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'filelist': // Basic File List (regex based crawler) + [$url, $filename] = self::getFromFileList($name, $pkg); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'url': // Direct download URL + $url = $pkg['url']; + $filename = $pkg['filename'] ?? basename($pkg['url']); + self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null); + break; + case 'git': // Git repo + self::downloadGit($name, $pkg['url'], $pkg['rev'], $pkg['extract'] ?? null); + break; + case 'custom': // Custom download method, like API-based download or other + $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\\store\\source'); + foreach ($classes as $class) { + if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { + (new $class())->fetch(); + break; + } + } + break; + default: + throw new DownloaderException('unknown source type: ' . $pkg['type']); + } + } catch (RuntimeException $e) { + // Because sometimes files downloaded through the command line are not automatically deleted after a failure. + // Here we need to manually delete the file if it is detected to exist. + if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { + logger()->warning('Deleting download file: ' . $filename); + unlink(DOWNLOAD_PATH . '/' . $filename); + } + throw new DownloaderException('Download failed! ' . $e->getMessage()); + } + } + /** * Download source by name and meta. * diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 7e5411ab..04468286 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -16,7 +16,7 @@ class FileSystem */ public static function loadConfigArray(string $config, ?string $config_dir = null): array { - $whitelist = ['ext', 'lib', 'source']; + $whitelist = ['ext', 'lib', 'source', 'pkg']; if (!in_array($config, $whitelist)) { throw new FileSystemException('Reading ' . $config . '.json is not allowed'); } @@ -138,6 +138,37 @@ class FileSystem } } + /** + * @throws RuntimeException + * @throws FileSystemException + */ + public static function extractPackage(string $name, string $filename, ?string $extract_path = null): void + { + if ($extract_path !== null) { + // replace + $extract_path = self::replacePathVariable($extract_path); + $extract_path = self::isRelativePath($extract_path) ? (WORKING_DIR . '/' . $extract_path) : $extract_path; + } else { + $extract_path = PKG_ROOT_PATH . '/' . $name; + } + logger()->info("extracting {$name} package to {$extract_path} ..."); + $target = self::convertPath($extract_path); + + if (!is_dir($dir = dirname($target))) { + self::createDir($dir); + } + try { + self::extractArchive($filename, $target); + } catch (RuntimeException $e) { + if (PHP_OS_FAMILY === 'Windows') { + f_passthru('rmdir /s /q ' . $target); + } else { + f_passthru('rm -r ' . $target); + } + throw new FileSystemException('Cannot extract package ' . $name, $e->getCode(), $e); + } + } + /** * 解压缩下载的资源包到 source 目录 * @@ -152,52 +183,24 @@ class FileSystem if (self::$_extract_hook === []) { SourcePatcher::init(); } - if (!is_dir(SOURCE_PATH)) { - self::createDir(SOURCE_PATH); - } if ($move_path !== null) { $move_path = SOURCE_PATH . '/' . $move_path; } logger()->info("extracting {$name} source to " . ($move_path ?? SOURCE_PATH . "/{$name}") . ' ...'); + $target = self::convertPath($move_path ?? (SOURCE_PATH . "/{$name}")); + if (!is_dir($dir = dirname($target))) { + self::createDir($dir); + } try { - $target = self::convertPath($move_path ?? (SOURCE_PATH . "/{$name}")); - // Git source, just move - if (is_dir(self::convertPath($filename))) { - self::copyDir(self::convertPath($filename), $target); - self::emitSourceExtractHook($name); - return; - } - if (f_mkdir(directory: $target, recursive: true) !== true) { - throw new FileSystemException('create ' . $name . 'source dir failed'); - } - - if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) { - match (self::extname($filename)) { - 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), - 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), - 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), - 'zip' => f_passthru("unzip {$filename} -d {$target}"), - default => throw new FileSystemException('unknown archive format: ' . $filename), - }; - } elseif (PHP_OS_FAMILY === 'Windows') { - // use php-sdk-binary-tools/bin/7za.exe - $_7z = self::convertPath(PHP_SDK_PATH . '/bin/7za.exe'); - f_mkdir(SOURCE_PATH . "/{$name}", recursive: true); - match (self::extname($filename)) { - 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), - 'xz', 'txz', 'gz', 'tgz', 'bz2' => f_passthru("\"{$_7z}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1"), - 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), - default => throw new FileSystemException("unknown archive format: {$filename}"), - }; - } + self::extractArchive($filename, $target); self::emitSourceExtractHook($name); } catch (RuntimeException $e) { if (PHP_OS_FAMILY === 'Windows') { - f_passthru('rmdir /s /q ' . SOURCE_PATH . "/{$name}"); + f_passthru('rmdir /s /q ' . $target); } else { - f_passthru('rm -r ' . SOURCE_PATH . "/{$name}"); + f_passthru('rm -r ' . $target); } - throw new FileSystemException('Cannot extract source ' . $name, $e->getCode(), $e); + throw new FileSystemException('Cannot extract source ' . $name . ': ' . $e->getMessage(), $e->getCode(), $e); } } @@ -411,6 +414,54 @@ class FileSystem return strlen($path) > 0 && $path[0] !== '/'; } + public static function replacePathVariable(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{php_sdk_path}' => defined('PHP_SDK_PATH') ? PHP_SDK_PATH : WORKING_DIR . '/php-sdk-binary-tools', + '{working_dir}' => WORKING_DIR, + '{download_path}' => DOWNLOAD_PATH, + '{source_path}' => SOURCE_PATH, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $path); + } + + /** + * @throws RuntimeException + * @throws FileSystemException + */ + private static function extractArchive(string $filename, string $target): void + { + // Git source, just move + if (is_dir(self::convertPath($filename))) { + self::copyDir(self::convertPath($filename), $target); + return; + } + // Create base dir + if (f_mkdir(directory: $target, recursive: true) !== true) { + throw new FileSystemException('create ' . $target . ' dir failed'); + } + + if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) { + match (self::extname($filename)) { + 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), + 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), + 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), + 'zip' => f_passthru("unzip {$filename} -d {$target}"), + default => throw new FileSystemException('unknown archive format: ' . $filename), + }; + } elseif (PHP_OS_FAMILY === 'Windows') { + // use php-sdk-binary-tools/bin/7za.exe + $_7z = self::convertPath(PHP_SDK_PATH . '/bin/7za.exe'); + match (self::extname($filename)) { + 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), + 'xz', 'txz', 'gz', 'tgz', 'bz2' => f_passthru("\"{$_7z}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1"), + 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), + default => throw new FileSystemException("unknown archive format: {$filename}"), + }; + } + } + /** * @throws FileSystemException */ diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php new file mode 100644 index 00000000..3eb68977 --- /dev/null +++ b/src/SPC/store/PackageManager.php @@ -0,0 +1,52 @@ + 'linux', + 'Windows' => 'win', + 'BSD' => 'freebsd', + 'Darwin' => 'macos', + default => throw new WrongUsageException('Unsupported OS!'), + }; + $config = Config::getPkg("{$pkg_name}-{$arch}-{$os}"); + } + if ($config === null) { + throw new WrongUsageException("Package [{$pkg_name}] does not exist, please check the name and correct it !"); + } + + // Download package + Downloader::downloadPackage($pkg_name, $config, $force); + // After download, read lock file name + $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); + $filename = DOWNLOAD_PATH . '/' . ($lock[$pkg_name]['filename'] ?? $lock[$pkg_name]['dirname']); + $extract = $lock[$pkg_name]['move_path'] === null ? (PKG_ROOT_PATH . '/' . $pkg_name) : $lock[$pkg_name]['move_path']; + FileSystem::extractPackage($pkg_name, $filename, $extract); + + // if contains extract-files, we just move this file to destination, and remove extract dir + if (is_array($config['extract-files'] ?? null) && is_assoc_array($config['extract-files'])) { + foreach ($config['extract-files'] as $file => $target) { + $target = FileSystem::convertPath(FileSystem::replacePathVariable($target)); + if (!is_dir($dir = dirname($target))) { + f_mkdir($dir, 0755, true); + } + logger()->debug("Moving package [{$pkg_name}] file {$file} to {$target}"); + rename(FileSystem::convertPath($extract . '/' . $file), $target); + } + FileSystem::removeDir($extract); + } + } +} diff --git a/src/SPC/store/SourceExtractor.php b/src/SPC/store/SourceManager.php similarity index 96% rename from src/SPC/store/SourceExtractor.php rename to src/SPC/store/SourceManager.php index 66a96c25..648c3d68 100644 --- a/src/SPC/store/SourceExtractor.php +++ b/src/SPC/store/SourceManager.php @@ -8,7 +8,7 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; -class SourceExtractor +class SourceManager { /** * @throws WrongUsageException @@ -59,7 +59,7 @@ class SourceExtractor } // check source dir exist - $check = $lock[$source]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (SOURCE_PATH . '/' . $lock[$source]['move_path']); + $check = $lock[$source]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (WORKING_DIR . '/' . $lock[$source]['move_path']); if (!is_dir($check)) { logger()->debug('Extracting source [' . $source . '] to ' . $check . ' ...'); FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$source]['filename'] ?? $lock[$source]['dirname']), $lock[$source]['move_path']); diff --git a/src/globals/defines.php b/src/globals/defines.php index aebb7f86..94a9ff9e 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -14,6 +14,7 @@ define('START_TIME', microtime(true)); define('BUILD_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'))); define('SOURCE_PATH', FileSystem::convertPath(is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'))); define('DOWNLOAD_PATH', FileSystem::convertPath(is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'))); +define('PKG_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('PKG_ROOT_PATH')) ? $a : (WORKING_DIR . '/pkgroot'))); define('BUILD_BIN_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_BIN_PATH')) ? $a : (BUILD_ROOT_PATH . '/bin'))); define('BUILD_LIB_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib'))); define('BUILD_INCLUDE_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include')));