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

View File

@@ -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。

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.
*

View File

@@ -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
*/

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\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']);