2023-03-15 20:40:49 +08:00
< ? php
declare ( strict_types = 1 );
namespace SPC\store ;
use SPC\exception\FileSystemException ;
use SPC\exception\RuntimeException ;
class FileSystem
{
2023-04-30 12:42:19 +08:00
private static array $_extract_hook = [];
2023-03-15 20:40:49 +08:00
/**
2025-07-29 11:08:53 +08:00
* Load configuration array from JSON file
*
2025-08-06 20:17:26 +08:00
* @ param string $config The configuration name ( ext , lib , source , pkg , pre - built )
* @ param null | string $config_dir Optional custom config directory
* @ return array The loaded configuration array
2023-03-15 20:40:49 +08:00
*/
2024-01-29 09:42:08 +08:00
public static function loadConfigArray ( string $config , ? string $config_dir = null ) : array
2023-03-15 20:40:49 +08:00
{
2024-07-07 20:45:18 +08:00
$whitelist = [ 'ext' , 'lib' , 'source' , 'pkg' , 'pre-built' ];
2023-03-15 20:40:49 +08:00
if ( ! in_array ( $config , $whitelist )) {
throw new FileSystemException ( 'Reading ' . $config . '.json is not allowed' );
}
2024-01-29 09:42:08 +08:00
$tries = $config_dir !== null ? [ FileSystem :: convertPath ( $config_dir . '/' . $config . '.json' )] : [
2023-03-15 20:40:49 +08:00
WORKING_DIR . '/config/' . $config . '.json' ,
ROOT_DIR . '/config/' . $config . '.json' ,
];
foreach ( $tries as $try ) {
if ( file_exists ( $try )) {
$json = json_decode ( self :: readFile ( $try ), true );
if ( ! is_array ( $json )) {
throw new FileSystemException ( 'Reading ' . $try . ' failed' );
}
return $json ;
}
}
throw new FileSystemException ( 'Reading ' . $config . '.json failed' );
}
/**
2025-07-29 11:08:53 +08:00
* Read file contents and throw exception on failure
2023-03-15 20:40:49 +08:00
*
2025-08-06 20:17:26 +08:00
* @ param string $filename The file path to read
* @ return string The file contents
2023-03-15 20:40:49 +08:00
*/
public static function readFile ( string $filename ) : string
{
// logger()->debug('Reading file: ' . $filename);
$r = file_get_contents ( self :: convertPath ( $filename ));
if ( $r === false ) {
throw new FileSystemException ( 'Reading file ' . $filename . ' failed' );
}
return $r ;
}
/**
2025-07-29 11:08:53 +08:00
* Replace string content in file
*
2025-08-06 20:17:26 +08:00
* @ param string $filename The file path
* @ param mixed $search The search string
* @ param mixed $replace The replacement string
* @ return false | int Number of replacements or false on failure
2023-03-15 20:40:49 +08:00
*/
2023-10-14 11:22:46 +08:00
public static function replaceFileStr ( string $filename , mixed $search = null , mixed $replace = null ) : false | int
2023-03-15 20:40:49 +08:00
{
2023-08-20 19:51:45 +08:00
return self :: replaceFile ( $filename , REPLACE_FILE_STR , $search , $replace );
}
/**
2025-07-29 11:08:53 +08:00
* Replace content in file using regex
*
2025-08-06 20:17:26 +08:00
* @ param string $filename The file path
* @ param mixed $search The regex pattern
* @ param mixed $replace The replacement string
* @ return false | int Number of replacements or false on failure
2023-08-20 19:51:45 +08:00
*/
2023-10-14 11:22:46 +08:00
public static function replaceFileRegex ( string $filename , mixed $search = null , mixed $replace = null ) : false | int
2023-08-20 19:51:45 +08:00
{
return self :: replaceFile ( $filename , REPLACE_FILE_PREG , $search , $replace );
}
/**
2025-07-29 11:08:53 +08:00
* Replace content in file using custom callback
*
2025-08-06 20:17:26 +08:00
* @ param string $filename The file path
* @ param mixed $callback The callback function
* @ return false | int Number of replacements or false on failure
2023-08-20 19:51:45 +08:00
*/
2023-10-14 11:22:46 +08:00
public static function replaceFileUser ( string $filename , mixed $callback = null ) : false | int
2023-08-20 19:51:45 +08:00
{
return self :: replaceFile ( $filename , REPLACE_FILE_USER , $callback );
2023-03-15 20:40:49 +08:00
}
/**
2025-07-29 11:08:53 +08:00
* Get file extension from filename
2023-03-15 20:40:49 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $fn The filename
* @ return string The file extension ( without dot )
2023-03-15 20:40:49 +08:00
*/
public static function extname ( string $fn ) : string
{
$parts = explode ( '.' , basename ( $fn ));
if ( count ( $parts ) < 2 ) {
return '' ;
}
return array_pop ( $parts );
}
/**
2025-07-29 11:08:53 +08:00
* Find command path in system PATH ( similar to which command )
2023-03-15 20:40:49 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $name The command name
* @ param array $paths Optional array of paths to search
* @ return null | string The full path to the command or null if not found
2023-03-15 20:40:49 +08:00
*/
public static function findCommandPath ( string $name , array $paths = []) : ? string
{
if ( ! $paths ) {
$paths = explode ( PATH_SEPARATOR , getenv ( 'PATH' ));
}
if ( PHP_OS_FAMILY === 'Windows' ) {
foreach ( $paths as $path ) {
foreach ([ '.exe' , '.bat' , '.cmd' ] as $suffix ) {
if ( file_exists ( $path . DIRECTORY_SEPARATOR . $name . $suffix )) {
return $path . DIRECTORY_SEPARATOR . $name . $suffix ;
}
}
}
return null ;
}
foreach ( $paths as $path ) {
if ( file_exists ( $path . DIRECTORY_SEPARATOR . $name )) {
return $path . DIRECTORY_SEPARATOR . $name ;
}
}
return null ;
}
2023-08-20 19:51:45 +08:00
/**
2025-07-29 11:08:53 +08:00
* Copy directory recursively
*
2025-08-06 20:17:26 +08:00
* @ param string $from Source directory path
* @ param string $to Destination directory path
2023-08-20 19:51:45 +08:00
*/
2023-03-15 20:40:49 +08:00
public static function copyDir ( string $from , string $to ) : void
{
2023-04-30 12:42:19 +08:00
$dst_path = FileSystem :: convertPath ( $to );
$src_path = FileSystem :: convertPath ( $from );
switch ( PHP_OS_FAMILY ) {
case 'Windows' :
f_passthru ( 'xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i' );
break ;
case 'Linux' :
case 'Darwin' :
2023-10-15 15:45:34 +08:00
case 'BSD' :
2023-04-30 12:42:19 +08:00
f_passthru ( 'cp -r "' . $src_path . '" "' . $dst_path . '"' );
break ;
2023-03-15 20:40:49 +08:00
}
}
2024-02-18 13:54:06 +08:00
/**
2025-07-29 11:08:53 +08:00
* Extract package archive to specified directory
*
2025-08-06 20:17:26 +08:00
* @ param string $name Package name
* @ param string $source_type Archive type ( tar . gz , zip , etc . )
* @ param string $filename Archive filename
* @ param null | string $extract_path Optional extraction path
2024-02-18 13:54:06 +08:00
*/
2025-06-14 16:09:48 +08:00
public static function extractPackage ( string $name , string $source_type , string $filename , ? string $extract_path = null ) : void
2024-02-18 13:54:06 +08:00
{
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 ;
}
2025-06-14 16:09:48 +08:00
logger () -> info ( " Extracting { $name } package to { $extract_path } ... " );
2024-02-18 13:54:06 +08:00
$target = self :: convertPath ( $extract_path );
if ( ! is_dir ( $dir = dirname ( $target ))) {
self :: createDir ( $dir );
}
try {
2025-06-14 16:09:48 +08:00
// extract wrapper command
self :: extractWithType ( $source_type , $filename , $extract_path );
2024-02-18 13:54:06 +08:00
} catch ( RuntimeException $e ) {
if ( PHP_OS_FAMILY === 'Windows' ) {
f_passthru ( 'rmdir /s /q ' . $target );
} else {
2024-02-19 12:15:52 +08:00
f_passthru ( 'rm -rf ' . $target );
2024-02-18 13:54:06 +08:00
}
throw new FileSystemException ( 'Cannot extract package ' . $name , $e -> getCode (), $e );
}
}
2023-03-15 20:40:49 +08:00
/**
2025-07-29 11:08:53 +08:00
* Extract source archive to source directory
2023-03-15 20:40:49 +08:00
*
2025-08-06 20:17:26 +08:00
* @ param string $name Source name
* @ param string $source_type Archive type ( tar . gz , zip , etc . )
* @ param string $filename Archive filename
* @ param null | string $move_path Optional move path
2023-03-15 20:40:49 +08:00
*/
2025-06-14 16:09:48 +08:00
public static function extractSource ( string $name , string $source_type , string $filename , ? string $move_path = null ) : void
2023-03-15 20:40:49 +08:00
{
2023-04-30 12:42:19 +08:00
// if source hook is empty, load it
if ( self :: $_extract_hook === []) {
SourcePatcher :: init ();
}
2025-06-14 16:09:48 +08:00
$move_path = match ( $move_path ) {
null => SOURCE_PATH . '/' . $name ,
default => self :: isRelativePath ( $move_path ) ? ( SOURCE_PATH . '/' . $move_path ) : $move_path ,
};
2024-02-22 14:37:10 +08:00
$target = self :: convertPath ( $move_path );
2025-06-14 16:09:48 +08:00
logger () -> info ( " Extracting { $name } source to { $target } " . ' ...' );
2024-02-18 13:54:06 +08:00
if ( ! is_dir ( $dir = dirname ( $target ))) {
self :: createDir ( $dir );
}
2023-03-15 20:40:49 +08:00
try {
2025-06-14 16:09:48 +08:00
self :: extractWithType ( $source_type , $filename , $move_path );
2024-06-30 22:34:27 +08:00
self :: emitSourceExtractHook ( $name , $target );
2023-03-15 20:40:49 +08:00
} catch ( RuntimeException $e ) {
if ( PHP_OS_FAMILY === 'Windows' ) {
2024-02-18 13:54:06 +08:00
f_passthru ( 'rmdir /s /q ' . $target );
2023-03-15 20:40:49 +08:00
} else {
2024-02-19 12:15:52 +08:00
f_passthru ( 'rm -rf ' . $target );
2023-03-15 20:40:49 +08:00
}
2024-02-18 13:54:06 +08:00
throw new FileSystemException ( 'Cannot extract source ' . $name . ': ' . $e -> getMessage (), $e -> getCode (), $e );
2023-03-15 20:40:49 +08:00
}
}
/**
2025-07-29 11:08:53 +08:00
* Convert path to system - specific format
2023-03-15 20:40:49 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $path The path to convert
* @ return string The converted path
2023-03-15 20:40:49 +08:00
*/
public static function convertPath ( string $path ) : string
{
if ( str_starts_with ( $path , 'phar://' )) {
return $path ;
}
return str_replace ( '/' , DIRECTORY_SEPARATOR , $path );
}
2025-07-29 11:08:53 +08:00
/**
* Convert Windows path to MinGW format
*
* @ param string $path The Windows path
* @ return string The MinGW format path
*/
2024-02-23 00:56:28 +08:00
public static function convertWinPathToMinGW ( string $path ) : string
{
if ( preg_match ( '/^[A-Za-z]:/' , $path )) {
$path = '/' . strtolower ( substr ( $path , 0 , 1 )) . '/' . str_replace ( '\\' , '/' , substr ( $path , 2 ));
}
return $path ;
}
2023-03-15 20:40:49 +08:00
/**
2025-07-29 11:08:53 +08:00
* Scan directory files recursively
2023-03-15 20:40:49 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $dir Directory to scan
* @ param bool $recursive Whether to scan recursively
* @ param bool | string $relative Whether to return relative paths
* @ param bool $include_dir Whether to include directories in result
* @ return array | false Array of files or false on failure
2023-03-15 20:40:49 +08:00
*/
2023-10-14 11:22:46 +08:00
public static function scanDirFiles ( string $dir , bool $recursive = true , bool | string $relative = false , bool $include_dir = false ) : array | false
2023-03-15 20:40:49 +08:00
{
$dir = self :: convertPath ( $dir );
if ( ! is_dir ( $dir )) {
return false ;
}
logger () -> debug ( 'scanning directory ' . $dir );
$scan_list = scandir ( $dir );
if ( $scan_list === false ) {
logger () -> warning ( 'Scan dir failed, cannot scan directory: ' . $dir );
return false ;
}
$list = [];
// 将 relative 置为相对目录的前缀
if ( $relative === true ) {
$relative = $dir ;
}
// 遍历目录
foreach ( $scan_list as $v ) {
// Unix 系统排除这俩目录
if ( $v == '.' || $v == '..' ) {
continue ;
}
$sub_file = self :: convertPath ( $dir . '/' . $v );
if ( is_dir ( $sub_file ) && $recursive ) {
# 如果是 目录 且 递推 , 则递推添加下级文件
$list = array_merge ( $list , self :: scanDirFiles ( $sub_file , $recursive , $relative ));
} elseif ( is_file ( $sub_file ) || is_dir ( $sub_file ) && ! $recursive && $include_dir ) {
# 如果是 文件 或 (是 目录 且 不递推 且 包含目录)
if ( is_string ( $relative ) && mb_strpos ( $sub_file , $relative ) === 0 ) {
$list [] = ltrim ( mb_substr ( $sub_file , mb_strlen ( $relative )), '/\\' );
} elseif ( $relative === false ) {
$list [] = $sub_file ;
}
}
}
return $list ;
}
/**
2025-07-29 11:08:53 +08:00
* Get PSR - 4 classes from directory
2023-03-18 17:32:21 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $dir Directory to scan
* @ param string $base_namespace Base namespace
* @ param mixed $rule Optional filtering rule
* @ param bool | string $return_path_value Whether to return path as value
* @ return array Array of class names or class => path pairs
2023-03-15 20:40:49 +08:00
*/
2023-03-18 17:32:21 +08:00
public static function getClassesPsr4 ( string $dir , string $base_namespace , mixed $rule = null , bool | string $return_path_value = false ) : array
2023-03-15 20:40:49 +08:00
{
$classes = [];
$files = FileSystem :: scanDirFiles ( $dir , true , true );
if ( $files === false ) {
throw new FileSystemException ( 'Cannot scan dir files during get classes psr-4 from dir: ' . $dir );
}
foreach ( $files as $v ) {
$pathinfo = pathinfo ( $v );
if (( $pathinfo [ 'extension' ] ? ? '' ) == 'php' ) {
2023-08-08 20:23:38 +08:00
if ( $rule === null ) {
2023-03-15 20:40:49 +08:00
if ( file_exists ( $dir . '/' . $pathinfo [ 'basename' ] . '.ignore' )) {
continue ;
}
if ( mb_substr ( $pathinfo [ 'basename' ], 0 , 7 ) == 'global_' || mb_substr ( $pathinfo [ 'basename' ], 0 , 7 ) == 'script_' ) {
continue ;
}
} elseif ( is_callable ( $rule ) && ! $rule ( $dir , $pathinfo )) {
continue ;
}
$dirname = $pathinfo [ 'dirname' ] == '.' ? '' : ( str_replace ( '/' , '\\' , $pathinfo [ 'dirname' ]) . '\\' );
$class_name = $base_namespace . '\\' . $dirname . $pathinfo [ 'filename' ];
if ( is_string ( $return_path_value )) {
$classes [ $class_name ] = $return_path_value . '/' . $v ;
} else {
$classes [] = $class_name ;
}
}
}
return $classes ;
}
/**
2025-07-29 11:08:53 +08:00
* Remove directory recursively
2023-03-15 20:40:49 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $dir Directory to remove
* @ return bool Success status
2023-03-15 20:40:49 +08:00
*/
2023-04-15 18:46:02 +08:00
public static function removeDir ( string $dir ) : bool
2023-03-15 20:40:49 +08:00
{
$dir = FileSystem :: convertPath ( $dir );
2023-04-15 18:46:02 +08:00
logger () -> debug ( 'Removing path recursively: "' . $dir . '"' );
2024-02-16 18:57:32 +08:00
if ( ! file_exists ( $dir )) {
logger () -> debug ( 'Scan dir failed, no such file or directory.' );
return false ;
}
2023-04-15 18:46:02 +08:00
if ( ! is_dir ( $dir )) {
2024-02-16 18:57:32 +08:00
logger () -> warning ( 'Scan dir failed, not directory.' );
2023-04-15 18:46:02 +08:00
return false ;
2023-03-15 20:40:49 +08:00
}
2023-04-15 18:46:02 +08:00
logger () -> debug ( 'scanning directory ' . $dir );
// 套上 zm_dir
$scan_list = scandir ( $dir );
if ( $scan_list === false ) {
logger () -> warning ( 'Scan dir failed, cannot scan directory: ' . $dir );
return false ;
}
// 遍历目录
foreach ( $scan_list as $v ) {
// Unix 系统排除这俩目录
if ( $v == '.' || $v == '..' ) {
continue ;
}
$sub_file = self :: convertPath ( $dir . '/' . $v );
if ( is_dir ( $sub_file )) {
# 如果是 目录 且 递推 , 则递推添加下级文件
if ( ! self :: removeDir ( $sub_file )) {
return false ;
}
} elseif ( is_link ( $sub_file ) || is_file ( $sub_file )) {
if ( ! unlink ( $sub_file )) {
return false ;
}
}
2023-03-15 20:40:49 +08:00
}
2024-11-06 16:45:45 +08:00
if ( is_link ( $dir )) {
return unlink ( $dir );
}
2023-04-15 18:46:02 +08:00
return rmdir ( $dir );
2023-03-15 20:40:49 +08:00
}
2023-03-26 22:27:51 +08:00
2023-08-20 19:51:45 +08:00
/**
2025-07-29 11:08:53 +08:00
* Create directory recursively
*
2025-08-06 20:17:26 +08:00
* @ param string $path Directory path to create
2023-08-20 19:51:45 +08:00
*/
2023-03-26 22:27:51 +08:00
public static function createDir ( string $path ) : void
{
2023-04-15 18:46:02 +08:00
if ( ! is_dir ( $path ) && ! f_mkdir ( $path , 0755 , true ) && ! is_dir ( $path )) {
2024-01-10 21:08:25 +08:00
throw new FileSystemException ( sprintf ( 'Unable to create dir: %s' , $path ));
2023-03-26 22:27:51 +08:00
}
}
2023-03-29 21:39:36 +08:00
2023-08-20 19:51:45 +08:00
/**
2025-07-29 11:08:53 +08:00
* Write content to file
*
2025-08-06 20:17:26 +08:00
* @ param string $path File path
* @ param mixed $content Content to write
* @ param mixed ... $args Additional arguments passed to file_put_contents
* @ return bool | int | string Result of file writing operation
2023-08-20 19:51:45 +08:00
*/
2023-10-14 11:22:46 +08:00
public static function writeFile ( string $path , mixed $content , ... $args ) : bool | int | string
2023-03-29 21:39:36 +08:00
{
2024-01-10 21:08:25 +08:00
$dir = pathinfo ( self :: convertPath ( $path ), PATHINFO_DIRNAME );
2023-03-29 21:39:36 +08:00
if ( ! is_dir ( $dir ) && ! mkdir ( $dir , 0755 , true )) {
throw new FileSystemException ( 'Write file failed, cannot create parent directory: ' . $dir );
}
return file_put_contents ( $path , $content , ... $args );
}
2023-04-29 18:59:47 +08:00
/**
2025-07-29 11:08:53 +08:00
* Reset directory by removing and recreating it
2023-04-29 18:59:47 +08:00
*
2025-08-06 20:17:26 +08:00
* @ param string $dir_name Directory name
2023-04-29 18:59:47 +08:00
*/
public static function resetDir ( string $dir_name ) : void
{
2025-07-29 11:08:53 +08:00
$dir_name = self :: convertPath ( $dir_name );
2023-04-29 18:59:47 +08:00
if ( is_dir ( $dir_name )) {
self :: removeDir ( $dir_name );
}
self :: createDir ( $dir_name );
}
2023-04-30 12:42:19 +08:00
2025-07-29 11:08:53 +08:00
/**
* Add source extraction hook
*
* @ param string $name Source name
* @ param callable $callback Callback function
*/
2023-08-20 19:51:45 +08:00
public static function addSourceExtractHook ( string $name , callable $callback ) : void
2023-04-30 12:42:19 +08:00
{
self :: $_extract_hook [ $name ][] = $callback ;
}
2023-07-20 01:15:28 +08:00
/**
2025-07-29 11:08:53 +08:00
* Check if path is relative
2023-07-20 01:15:28 +08:00
*
2025-07-29 11:08:53 +08:00
* @ param string $path Path to check
* @ return bool True if path is relative
2023-07-20 01:15:28 +08:00
*/
public static function isRelativePath ( string $path ) : bool
{
if ( DIRECTORY_SEPARATOR === '\\' ) {
return ! ( strlen ( $path ) > 2 && ctype_alpha ( $path [ 0 ]) && $path [ 1 ] === ':' );
}
return strlen ( $path ) > 0 && $path [ 0 ] !== '/' ;
}
2025-07-29 11:08:53 +08:00
/**
* Replace path variables with actual values
*
* @ param string $path Path with variables
* @ return string Path with replaced variables
*/
2024-02-18 13:54:06 +08:00
public static function replacePathVariable ( string $path ) : string
{
$replacement = [
'{pkg_root_path}' => PKG_ROOT_PATH ,
2024-10-03 10:44:49 +08:00
'{php_sdk_path}' => getenv ( 'PHP_SDK_PATH' ) ? getenv ( 'PHP_SDK_PATH' ) : WORKING_DIR . '/php-sdk-binary-tools' ,
2024-02-18 13:54:06 +08:00
'{working_dir}' => WORKING_DIR ,
'{download_path}' => DOWNLOAD_PATH ,
'{source_path}' => SOURCE_PATH ,
];
return str_replace ( array_keys ( $replacement ), array_values ( $replacement ), $path );
}
2025-07-29 11:08:53 +08:00
/**
* Create backup of file
*
* @ param string $path File path
* @ return string Backup file path
*/
2024-06-03 23:16:15 +08:00
public static function backupFile ( string $path ) : string
{
copy ( $path , $path . '.bak' );
return $path . '.bak' ;
}
2025-07-29 11:08:53 +08:00
/**
* Restore file from backup
*
* @ param string $path Original file path
*/
2024-06-03 23:16:15 +08:00
public static function restoreBackupFile ( string $path ) : void
{
if ( ! file_exists ( $path . '.bak' )) {
throw new RuntimeException ( 'Cannot find bak file for ' . $path );
}
copy ( $path . '.bak' , $path );
unlink ( $path . '.bak' );
}
2025-07-29 11:08:53 +08:00
/**
* Remove file if it exists
*
* @ param string $string File path
*/
2025-03-10 16:15:47 +08:00
public static function removeFileIfExists ( string $string ) : void
{
2025-07-29 11:08:53 +08:00
$string = self :: convertPath ( $string );
2025-03-10 16:15:47 +08:00
if ( file_exists ( $string )) {
unlink ( $string );
}
}
2025-03-30 14:01:31 +08:00
/**
2025-07-29 11:08:53 +08:00
* Replace line in file that contains specific string
*
2025-08-06 20:17:26 +08:00
* @ param string $file File path
* @ param string $find String to find in line
* @ param string $line New line content
* @ return false | int Number of replacements or false on failure
2025-03-30 14:01:31 +08:00
*/
public static function replaceFileLineContainsString ( string $file , string $find , string $line ) : false | int
{
$lines = file ( $file );
if ( $lines === false ) {
throw new FileSystemException ( 'Cannot read file: ' . $file );
}
foreach ( $lines as $key => $value ) {
if ( str_contains ( $value , $find )) {
$lines [ $key ] = $line . PHP_EOL ;
}
}
return file_put_contents ( $file , implode ( '' , $lines ));
}
2024-02-18 13:54:06 +08:00
private static function extractArchive ( string $filename , string $target ) : void
{
// Create base dir
if ( f_mkdir ( directory : $target , recursive : true ) !== true ) {
throw new FileSystemException ( 'create ' . $target . ' dir failed' );
}
2024-02-23 00:56:28 +08:00
if ( ! file_exists ( $filename )) {
throw new FileSystemException ( 'File not exists' );
}
2024-02-18 13:54:06 +08:00
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
2024-10-03 10:44:49 +08:00
$_7z = self :: convertPath ( getenv ( 'PHP_SDK_PATH' ) . '/bin/7za.exe' );
2024-02-23 00:56:28 +08:00
// Windows notes: I hate windows tar.......
// When extracting .tar.gz like libxml2, it shows a symlink error and returns code[1].
// Related posts: https://answers.microsoft.com/en-us/windows/forum/all/tar-on-windows-fails-to-extract-archive-containing/0ee9a7ea-9b1f-4fef-86a9-5d9dc35cea2f
// And MinGW tar.exe cannot work on temporarily storage ??? (GitHub Actions hosted runner)
// Yeah, I will be an MS HATER !
2024-02-18 13:54:06 +08:00
match ( self :: extname ( $filename )) {
'tar' => f_passthru ( " tar -xf { $filename } -C { $target } --strip-components 1 " ),
2024-02-23 00:56:28 +08:00
'xz' , 'txz' , 'gz' , 'tgz' , 'bz2' => cmd () -> execWithResult ( " \" { $_7z } \" x -so { $filename } | tar -f - -x -C \" { $target } \" --strip-components 1 " ),
2024-02-18 13:54:06 +08:00
'zip' => f_passthru ( " \" { $_7z } \" x { $filename } -o { $target } -y " ),
default => throw new FileSystemException ( " unknown archive format: { $filename } " ),
};
}
}
2023-10-14 11:22:46 +08:00
private static function replaceFile ( string $filename , int $replace_type = REPLACE_FILE_STR , mixed $callback_or_search = null , mixed $to_replace = null ) : false | int
2023-08-20 19:51:45 +08:00
{
logger () -> debug ( 'Replacing file with type[' . $replace_type . ']: ' . $filename );
$file = self :: readFile ( $filename );
switch ( $replace_type ) {
case REPLACE_FILE_STR :
default :
$file = str_replace ( $callback_or_search , $to_replace , $file );
break ;
case REPLACE_FILE_PREG :
$file = preg_replace ( $callback_or_search , $to_replace , $file );
break ;
case REPLACE_FILE_USER :
$file = $callback_or_search ( $file );
break ;
}
return file_put_contents ( $filename , $file );
}
2024-06-30 22:34:27 +08:00
private static function emitSourceExtractHook ( string $name , string $target ) : void
2023-04-30 12:42:19 +08:00
{
foreach (( self :: $_extract_hook [ $name ] ? ? []) as $hook ) {
2024-06-30 22:34:27 +08:00
if ( $hook ( $name , $target ) === true ) {
2023-04-30 12:42:19 +08:00
logger () -> info ( 'Patched source [' . $name . '] after extracted' );
}
}
}
2025-06-14 16:09:48 +08:00
private static function extractWithType ( string $source_type , string $filename , string $extract_path ) : void
{
logger () -> debug ( 'Extracting source [' . $source_type . ']: ' . $filename );
/* @phpstan-ignore-next-line */
match ( $source_type ) {
SPC_SOURCE_ARCHIVE => self :: extractArchive ( $filename , $extract_path ),
SPC_SOURCE_GIT => self :: copyDir ( self :: convertPath ( $filename ), $extract_path ),
// soft link to the local source
SPC_SOURCE_LOCAL => symlink ( self :: convertPath ( $filename ), $extract_path ),
};
}
2023-03-15 20:40:49 +08:00
}