Add package management

This commit is contained in:
crazywhalecc 2024-02-18 13:54:06 +08:00 committed by Jerry Ma
parent 78f4317660
commit a30e054d7d
18 changed files with 481 additions and 85 deletions

3
.gitignore vendored
View File

@ -16,6 +16,9 @@ docker/source/
# default source build root directory # default source build root directory
/buildroot/ /buildroot/
# default package root directory
/pkgroot/
# tools cache files # tools cache files
.php-cs-fixer.cache .php-cs-fixer.cache
.phpunit.result.cache .phpunit.result.cache

32
config/pkg.json Normal file
View File

@ -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"
}
}

View File

@ -6,6 +6,7 @@ namespace SPC;
use SPC\command\BuildCliCommand; use SPC\command\BuildCliCommand;
use SPC\command\BuildLibsCommand; use SPC\command\BuildLibsCommand;
use SPC\command\DeleteDownloadCommand;
use SPC\command\dev\AllExtCommand; use SPC\command\dev\AllExtCommand;
use SPC\command\dev\PhpVerCommand; use SPC\command\dev\PhpVerCommand;
use SPC\command\dev\SortConfigCommand; use SPC\command\dev\SortConfigCommand;
@ -13,6 +14,7 @@ use SPC\command\DoctorCommand;
use SPC\command\DownloadCommand; use SPC\command\DownloadCommand;
use SPC\command\DumpLicenseCommand; use SPC\command\DumpLicenseCommand;
use SPC\command\ExtractCommand; use SPC\command\ExtractCommand;
use SPC\command\InstallPkgCommand;
use SPC\command\MicroCombineCommand; use SPC\command\MicroCombineCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\HelpCommand;
@ -23,7 +25,7 @@ use Symfony\Component\Console\Command\ListCommand;
*/ */
final class ConsoleApplication extends Application final class ConsoleApplication extends Application
{ {
public const VERSION = '2.1.0-beta.3'; public const VERSION = '2.1.0-beta.4';
public function __construct() public function __construct()
{ {
@ -35,6 +37,8 @@ final class ConsoleApplication extends Application
new BuildLibsCommand(), new BuildLibsCommand(),
new DoctorCommand(), new DoctorCommand(),
new DownloadCommand(), new DownloadCommand(),
new InstallPkgCommand(),
new DeleteDownloadCommand(),
new DumpLicenseCommand(), new DumpLicenseCommand(),
new ExtractCommand(), new ExtractCommand(),
new MicroCombineCommand(), new MicroCombineCommand(),

View File

@ -9,7 +9,7 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException; use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\SourceExtractor; use SPC\store\SourceManager;
use SPC\util\CustomExt; use SPC\util\CustomExt;
abstract class BuilderBase abstract class BuilderBase
@ -144,15 +144,15 @@ abstract class BuilderBase
{ {
CustomExt::loadCustomExt(); CustomExt::loadCustomExt();
$this->emitPatchPoint('before-php-extract'); $this->emitPatchPoint('before-php-extract');
SourceExtractor::initSource(sources: ['php-src']); SourceManager::initSource(sources: ['php-src']);
$this->emitPatchPoint('after-php-extract'); $this->emitPatchPoint('after-php-extract');
if ($this->getPHPVersionID() >= 80000) { if ($this->getPHPVersionID() >= 80000) {
$this->emitPatchPoint('before-micro-extract'); $this->emitPatchPoint('before-micro-extract');
SourceExtractor::initSource(sources: ['micro']); SourceManager::initSource(sources: ['micro']);
$this->emitPatchPoint('after-micro-extract'); $this->emitPatchPoint('after-micro-extract');
} }
$this->emitPatchPoint('before-exts-extract'); $this->emitPatchPoint('before-exts-extract');
SourceExtractor::initSource(exts: $extensions); SourceManager::initSource(exts: $extensions);
$this->emitPatchPoint('after-exts-extract'); $this->emitPatchPoint('after-exts-extract');
foreach ($extensions as $extension) { foreach ($extensions as $extension) {
$class = CustomExt::getExtClass($extension); $class = CustomExt::getExtClass($extension);

View File

@ -11,7 +11,7 @@ use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\SourceExtractor; use SPC\store\SourceManager;
use SPC\util\DependencyUtil; use SPC\util\DependencyUtil;
abstract class UnixBuilderBase extends BuilderBase abstract class UnixBuilderBase extends BuilderBase
@ -134,7 +134,7 @@ abstract class UnixBuilderBase extends BuilderBase
$this->emitPatchPoint('before-libs-extract'); $this->emitPatchPoint('before-libs-extract');
// extract sources // extract sources
SourceExtractor::initSource(libs: $sorted_libraries); SourceManager::initSource(libs: $sorted_libraries);
$this->emitPatchPoint('after-libs-extract'); $this->emitPatchPoint('after-libs-extract');

View File

@ -10,7 +10,7 @@ use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\SourceExtractor; use SPC\store\SourceManager;
use SPC\store\SourcePatcher; use SPC\store\SourcePatcher;
use SPC\util\DependencyUtil; use SPC\util\DependencyUtil;
@ -211,7 +211,7 @@ class WindowsBuilder extends BuilderBase
} }
// extract sources // extract sources
SourceExtractor::initSource(libs: $sorted_libraries); SourceManager::initSource(libs: $sorted_libraries);
// build all libs // build all libs
foreach ($this->libs as $lib) { foreach ($this->libs as $lib) {

View File

@ -14,7 +14,8 @@ class openssl extends WindowsLibraryBase
protected function build(): void 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) { if ($perl === null) {
throw new RuntimeException('You need to install perl first! (easiest way is using static-php-cli command "doctor")'); throw new RuntimeException('You need to install perl first! (easiest way is using static-php-cli command "doctor")');
} }

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('del-download', 'Remove locked download source or package using name', ['delete-download', 'del-down'])]
class DeleteDownloadCommand extends BaseCommand
{
public function configure(): void
{
$this->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;
}
}
}

View File

@ -8,11 +8,11 @@ use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\FileSystemException; use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException; use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\SourceExtractor; use SPC\store\SourceManager;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
#[AsCommand('extract', 'Extract required sources')] #[AsCommand('extract', 'Extract required sources', ['extract-source'])]
class ExtractCommand extends BaseCommand class ExtractCommand extends BaseCommand
{ {
use UnixSystemUtilTrait; use UnixSystemUtilTrait;
@ -34,7 +34,7 @@ class ExtractCommand extends BaseCommand
$this->output->writeln('<error>sources cannot be empty, at least contain one !</error>'); $this->output->writeln('<error>sources cannot be empty, at least contain one !</error>');
return static::FAILURE; return static::FAILURE;
} }
SourceExtractor::initSource(sources: $sources); SourceManager::initSource(sources: $sources);
logger()->info('Extract done !'); logger()->info('Extract done !');
return static::SUCCESS; return static::SUCCESS;
} }

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\builder\traits\UnixSystemUtilTrait;
use SPC\exception\DownloaderException;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\PackageManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand('install-pkg', 'Install additional packages', ['i', 'install-package'])]
class InstallPkgCommand extends BaseCommand
{
use UnixSystemUtilTrait;
public function configure(): void
{
$this->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;
}
}
}

View File

@ -14,6 +14,7 @@ use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Downloader; use SPC\store\Downloader;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\PackageManager;
class LinuxMuslCheck class LinuxMuslCheck
{ {
@ -84,7 +85,6 @@ class LinuxMuslCheck
/** @noinspection PhpUnused */ /** @noinspection PhpUnused */
/** /**
* @throws DownloaderException
* @throws FileSystemException * @throws FileSystemException
* @throws WrongUsageException * @throws WrongUsageException
*/ */
@ -98,15 +98,10 @@ class LinuxMuslCheck
logger()->warning('Current user is not root, using sudo for running command'); logger()->warning('Current user is not root, using sudo for running command');
} }
$arch = arch2gnu(php_uname('m')); $arch = arch2gnu(php_uname('m'));
$musl_compile_source = [ PackageManager::installPackage("musl-toolchain-{$arch}-linux");
'type' => 'url', $pkg_root = PKG_ROOT_PATH . "/musl-toolchain-{$arch}-linux";
'url' => "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/{$arch}-musl-toolchain.tgz", shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl");
]; FileSystem::removeDir($pkg_root);
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');
return true; return true;
} catch (RuntimeException) { } catch (RuntimeException) {
return false; return false;

View File

@ -9,8 +9,8 @@ use SPC\doctor\AsCheckItem;
use SPC\doctor\AsFixItem; use SPC\doctor\AsFixItem;
use SPC\doctor\CheckResult; use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException; use SPC\exception\RuntimeException;
use SPC\store\Downloader;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use SPC\store\PackageManager;
class WindowsToolCheckList class WindowsToolCheckList
{ {
@ -64,8 +64,9 @@ class WindowsToolCheckList
#[AsCheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)] #[AsCheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
public function checkPerl(): ?CheckResult public function checkPerl(): ?CheckResult
{ {
if (file_exists(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe')) { $arch = arch2gnu(php_uname('m'));
return CheckResult::ok(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe'); 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) { if (($path = SystemUtil::findCommand('perl.exe')) === null) {
return CheckResult::fail('perl not found in path.', 'install-perl'); return CheckResult::fail('perl not found in path.', 'install-perl');
@ -91,32 +92,15 @@ class WindowsToolCheckList
#[AsFixItem('install-nasm')] #[AsFixItem('install-nasm')]
public function installNasm(): bool public function installNasm(): bool
{ {
// The hardcoded version here is to be consistent with the version compiled by `musl-cross-toolchain`. PackageManager::installPackage('nasm-x86_64-win');
$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');
return true; return true;
} }
#[AsFixItem('install-perl')] #[AsFixItem('install-perl')]
public function installPerl(): bool 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'; $arch = arch2gnu(php_uname('m'));
$source = [ PackageManager::installPackage("strawberry-perl-{$arch}-win");
'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');
return true; return true;
} }
} }

View File

@ -12,6 +12,8 @@ use SPC\exception\WrongUsageException;
*/ */
class Config class Config
{ {
public static ?array $pkg = null;
public static ?array $source = null; public static ?array $source = null;
public static ?array $lib = null; public static ?array $lib = null;
@ -31,6 +33,19 @@ class Config
return self::$source[$name] ?? null; 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 库依赖项 * 根据不同的操作系统分别选择不同的 lib 库依赖项
* 如果 key null,那么直接返回整个 meta。 * 如果 key null,那么直接返回整个 meta。

View File

@ -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. * Download source by name and meta.
* *

View File

@ -16,7 +16,7 @@ class FileSystem
*/ */
public static function loadConfigArray(string $config, ?string $config_dir = null): array 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)) { if (!in_array($config, $whitelist)) {
throw new FileSystemException('Reading ' . $config . '.json is not allowed'); 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 目录 * 解压缩下载的资源包到 source 目录
* *
@ -152,52 +183,24 @@ class FileSystem
if (self::$_extract_hook === []) { if (self::$_extract_hook === []) {
SourcePatcher::init(); SourcePatcher::init();
} }
if (!is_dir(SOURCE_PATH)) {
self::createDir(SOURCE_PATH);
}
if ($move_path !== null) { if ($move_path !== null) {
$move_path = SOURCE_PATH . '/' . $move_path; $move_path = SOURCE_PATH . '/' . $move_path;
} }
logger()->info("extracting {$name} source to " . ($move_path ?? SOURCE_PATH . "/{$name}") . ' ...'); 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 { try {
$target = self::convertPath($move_path ?? (SOURCE_PATH . "/{$name}")); self::extractArchive($filename, $target);
// 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::emitSourceExtractHook($name); self::emitSourceExtractHook($name);
} catch (RuntimeException $e) { } catch (RuntimeException $e) {
if (PHP_OS_FAMILY === 'Windows') { if (PHP_OS_FAMILY === 'Windows') {
f_passthru('rmdir /s /q ' . SOURCE_PATH . "/{$name}"); f_passthru('rmdir /s /q ' . $target);
} else { } 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] !== '/'; 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 * @throws FileSystemException
*/ */

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace SPC\store;
use SPC\exception\WrongUsageException;
class PackageManager
{
public static function installPackage(string $pkg_name, ?array $config = null, bool $force = false): void
{
if ($config === null) {
$config = Config::getPkg($pkg_name);
}
if ($config === null) {
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Linux' => '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);
}
}
}

View File

@ -8,7 +8,7 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException; use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
class SourceExtractor class SourceManager
{ {
/** /**
* @throws WrongUsageException * @throws WrongUsageException
@ -59,7 +59,7 @@ class SourceExtractor
} }
// check source dir exist // 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)) { if (!is_dir($check)) {
logger()->debug('Extracting source [' . $source . '] to ' . $check . ' ...'); logger()->debug('Extracting source [' . $source . '] to ' . $check . ' ...');
FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$source]['filename'] ?? $lock[$source]['dirname']), $lock[$source]['move_path']); FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$source]['filename'] ?? $lock[$source]['dirname']), $lock[$source]['move_path']);

View File

@ -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('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('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('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_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_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'))); define('BUILD_INCLUDE_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include')));