2023-04-30 12:42:19 +08:00
< ? php
declare ( strict_types = 1 );
namespace SPC\command ;
2023-07-17 21:36:50 +08:00
use SPC\builder\traits\UnixSystemUtilTrait ;
2023-04-30 12:42:19 +08:00
use SPC\exception\DownloaderException ;
2025-08-06 20:45:16 +08:00
use SPC\exception\SPCException ;
2023-04-30 12:42:19 +08:00
use SPC\store\Config ;
use SPC\store\Downloader ;
2026-01-01 14:31:35 +01:00
use SPC\store\FileSystem ;
2025-06-19 09:50:38 +07:00
use SPC\store\LockFile ;
2026-01-01 14:31:35 +01:00
use SPC\store\source\CustomSourceBase ;
2023-10-22 16:35:15 +08:00
use SPC\util\DependencyUtil ;
2025-06-28 16:36:05 +08:00
use SPC\util\SPCTarget ;
2023-04-30 12:42:19 +08:00
use Symfony\Component\Console\Attribute\AsCommand ;
use Symfony\Component\Console\Input\InputArgument ;
use Symfony\Component\Console\Input\InputInterface ;
use Symfony\Component\Console\Input\InputOption ;
use Symfony\Component\Console\Output\OutputInterface ;
#[AsCommand('download', 'Download required sources', ['fetch'])]
class DownloadCommand extends BaseCommand
{
2023-07-17 21:36:50 +08:00
use UnixSystemUtilTrait ;
2023-04-30 12:42:19 +08:00
protected string $php_major_ver ;
2023-08-20 19:51:45 +08:00
public function configure () : void
2023-04-30 12:42:19 +08:00
{
2026-01-01 14:31:35 +01:00
$this -> addArgument ( 'sources' , InputArgument :: OPTIONAL , 'The sources will be compiled, comma separated' );
2023-04-30 12:42:19 +08:00
$this -> addOption ( 'shallow-clone' , null , null , 'Clone shallow' );
$this -> addOption ( 'with-openssl11' , null , null , 'Use openssl 1.1' );
2026-01-01 13:56:01 +01:00
$this -> addOption ( 'with-php' , null , InputOption :: VALUE_REQUIRED , 'version in major.minor format, comma-separated for multiple versions (default 8.4)' , '8.4' );
2023-04-30 12:42:19 +08:00
$this -> addOption ( 'clean' , null , null , 'Clean old download cache and source before fetch' );
$this -> addOption ( 'all' , 'A' , null , 'Fetch all sources that static-php-cli needed' );
2023-07-28 00:02:49 +08:00
$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"' );
2024-09-05 00:00:58 +08:00
$this -> addOption ( 'custom-git' , 'G' , InputOption :: VALUE_IS_ARRAY | InputOption :: VALUE_REQUIRED , 'Specify custom source git url, e.g "php-src:master:https://github.com/php/php-src.git"' );
2023-07-17 21:36:50 +08:00
$this -> addOption ( 'from-zip' , 'Z' , InputOption :: VALUE_REQUIRED , 'Fetch from zip archive' );
2023-10-29 00:48:30 +02:00
$this -> addOption ( 'for-extensions' , 'e' , InputOption :: VALUE_REQUIRED , 'Fetch by extensions, e.g "openssl,mbstring"' );
2024-03-05 21:43:09 +08:00
$this -> addOption ( 'for-libs' , 'l' , InputOption :: VALUE_REQUIRED , 'Fetch by libraries, e.g "libcares,openssl,onig"' );
2023-10-29 00:48:30 +02:00
$this -> addOption ( 'without-suggestions' , null , null , 'Do not fetch suggested sources when using --for-extensions' );
2024-07-07 20:45:18 +08:00
$this -> addOption ( 'ignore-cache-sources' , null , InputOption :: VALUE_OPTIONAL , 'Ignore some source caches, comma separated, e.g "php-src,curl,openssl"' , false );
2024-03-10 16:23:30 +08:00
$this -> addOption ( 'retry' , 'R' , InputOption :: VALUE_REQUIRED , 'Set retry time when downloading failed (default: 0)' , '0' );
2024-07-07 20:45:18 +08:00
$this -> addOption ( 'prefer-pre-built' , 'P' , null , 'Download pre-built libraries when available' );
2025-04-22 20:25:44 +08:00
$this -> addOption ( 'no-alt' , null , null , 'Do not download alternative sources' );
2026-01-01 14:31:35 +01:00
$this -> addOption ( 'update' , null , null , 'Check and update downloaded sources' );
2023-04-30 12:42:19 +08:00
}
2023-08-20 19:51:45 +08:00
public function initialize ( InputInterface $input , OutputInterface $output ) : void
2023-04-30 12:42:19 +08:00
{
2026-01-01 14:31:35 +01:00
// mode: --update
if ( $input -> getOption ( 'update' ) && empty ( $input -> getArgument ( 'sources' )) && empty ( $input -> getOption ( 'for-extensions' )) && empty ( $input -> getOption ( 'for-libs' ))) {
if ( ! file_exists ( LockFile :: LOCK_FILE )) {
parent :: initialize ( $input , $output );
return ;
}
$lock_content = json_decode ( file_get_contents ( LockFile :: LOCK_FILE ), true );
if ( is_array ( $lock_content )) {
// Filter out pre-built sources
$sources_to_check = array_filter ( $lock_content , function ( $name ) {
return
! str_contains ( $name , '-Linux-' ) &&
! str_contains ( $name , '-Windows-' ) &&
! str_contains ( $name , '-Darwin-' );
});
$input -> setArgument ( 'sources' , implode ( ',' , array_keys ( $sources_to_check )));
}
parent :: initialize ( $input , $output );
return ;
}
2024-03-10 10:58:58 +08:00
// mode: --all
if ( $input -> getOption ( 'all' )) {
$input -> setArgument ( 'sources' , implode ( ',' , array_keys ( Config :: getSources ())));
parent :: initialize ( $input , $output );
return ;
}
// mode: --clean and --from-zip
if ( $input -> getOption ( 'clean' ) || $input -> getOption ( 'from-zip' )) {
2023-04-30 12:42:19 +08:00
$input -> setArgument ( 'sources' , '' );
2024-03-10 10:58:58 +08:00
parent :: initialize ( $input , $output );
return ;
}
// mode: normal
if ( ! empty ( $input -> getArgument ( 'sources' ))) {
$final_sources = array_map ( 'trim' , array_filter ( explode ( ',' , $input -> getArgument ( 'sources' ))));
} else {
$final_sources = [];
}
// mode: --for-extensions
if ( $for_ext = $input -> getOption ( 'for-extensions' )) {
2024-06-05 23:20:21 +08:00
$ext = $this -> parseExtensionList ( $for_ext );
2024-03-10 10:58:58 +08:00
$sources = $this -> calculateSourcesByExt ( $ext , ! $input -> getOption ( 'without-suggestions' ));
$final_sources = array_merge ( $final_sources , array_diff ( $sources , $final_sources ));
}
// mode: --for-libs
if ( $for_lib = $input -> getOption ( 'for-libs' )) {
$lib = array_map ( 'trim' , array_filter ( explode ( ',' , $for_lib )));
$sources = $this -> calculateSourcesByLib ( $lib , ! $input -> getOption ( 'without-suggestions' ));
$final_sources = array_merge ( $final_sources , array_diff ( $sources , $final_sources ));
}
if ( ! empty ( $final_sources )) {
$input -> setArgument ( 'sources' , implode ( ',' , $final_sources ));
2023-04-30 12:42:19 +08:00
}
parent :: initialize ( $input , $output );
}
public function handle () : int
{
2025-08-06 20:45:16 +08:00
if ( $this -> getOption ( 'clean' )) {
return $this -> _clean ();
}
2023-04-30 12:42:19 +08:00
2025-08-06 20:45:16 +08:00
// --from-zip
if ( $path = $this -> getOption ( 'from-zip' )) {
return $this -> downloadFromZip ( $path );
}
2023-07-17 21:36:50 +08:00
2026-01-01 14:31:35 +01:00
if ( $this -> getOption ( 'update' )) {
return $this -> handleUpdate ();
}
2026-01-01 13:56:01 +01:00
// Define PHP major version(s)
$php_versions_str = $this -> getOption ( 'with-php' );
$php_versions = array_map ( 'trim' , explode ( ',' , $php_versions_str ));
// Validate all versions
foreach ( $php_versions as $ver ) {
if ( $ver !== 'git' && ! preg_match ( '/^\d+\.\d+$/' , $ver )) {
// If not git, we need to check the version format
if ( ! preg_match ( '/^\d+\.\d+(\.\d+)?$/' , $ver )) {
logger () -> error ( " bad version arg: { $ver } , x.y or x.y.z required! " );
return static :: FAILURE ;
}
2023-10-23 20:12:47 +08:00
}
2025-08-06 20:45:16 +08:00
}
2023-04-30 12:42:19 +08:00
2026-01-01 13:56:01 +01:00
// Set the first version as the default for backward compatibility
$this -> php_major_ver = $php_versions [ 0 ];
define ( 'SPC_BUILD_PHP_VERSION' , $this -> php_major_ver );
2025-08-06 20:45:16 +08:00
// retry
2025-09-21 19:59:11 +02:00
$retry = ( int ) $this -> getOption ( 'retry' );
2025-08-06 20:45:16 +08:00
f_putenv ( 'SPC_DOWNLOAD_RETRIES=' . $retry );
2024-03-10 16:23:30 +08:00
2025-08-06 20:45:16 +08:00
// Use shallow-clone can reduce git resource download
if ( $this -> getOption ( 'shallow-clone' )) {
define ( 'GIT_SHALLOW_CLONE' , true );
}
2023-04-30 12:42:19 +08:00
2025-08-06 20:45:16 +08:00
// To read config
Config :: getSource ( 'openssl' );
2023-04-30 12:42:19 +08:00
2025-08-06 20:45:16 +08:00
// use openssl 1.1
if ( $this -> getOption ( 'with-openssl11' )) {
logger () -> debug ( 'Using openssl 1.1' );
Config :: $source [ 'openssl' ][ 'regex' ] = '/href="(?<file>openssl-(?<version>1.[^"]+)\.tar\.gz)\"/' ;
}
2023-04-30 12:42:19 +08:00
2025-08-06 20:45:16 +08:00
$chosen_sources = array_map ( 'trim' , array_filter ( explode ( ',' , $this -> getArgument ( 'sources' ))));
2024-07-07 20:45:18 +08:00
2026-01-01 13:56:01 +01:00
// Handle multiple PHP versions
// If php-src is in the sources, replace it with version-specific sources
if ( in_array ( 'php-src' , $chosen_sources )) {
// Remove php-src from the list
$chosen_sources = array_diff ( $chosen_sources , [ 'php-src' ]);
// Add version-specific php-src for each version
foreach ( $php_versions as $ver ) {
$version_specific_name = " php-src- { $ver } " ;
$chosen_sources [] = $version_specific_name ;
// Store the version for this specific php-src
f_putenv ( " SPC_PHP_VERSION_ { $version_specific_name } = { $ver } " );
}
}
2025-08-06 20:45:16 +08:00
$sss = $this -> getOption ( 'ignore-cache-sources' );
if ( $sss === false ) {
// false is no-any-ignores, that is, default.
$force_all = false ;
$force_list = [];
} elseif ( $sss === null ) {
// null means all sources will be ignored, equals to --force-all (but we don't want to add too many options)
$force_all = true ;
$force_list = [];
} else {
// ignore some sources
$force_all = false ;
$force_list = array_map ( 'trim' , array_filter ( explode ( ',' , $this -> getOption ( 'ignore-cache-sources' ))));
}
2024-07-07 20:45:18 +08:00
2025-08-06 20:45:16 +08:00
if ( $this -> getOption ( 'all' )) {
logger () -> notice ( 'Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !' );
}
2023-04-30 12:42:19 +08:00
2025-08-06 20:45:16 +08:00
// Process -U options
$custom_urls = [];
foreach ( $this -> input -> getOption ( 'custom-url' ) as $value ) {
[ $source_name , $url ] = explode ( ':' , $value , 2 );
$custom_urls [ $source_name ] = $url ;
}
// Process -G options
$custom_gits = [];
foreach ( $this -> input -> getOption ( 'custom-git' ) as $value ) {
[ $source_name , $branch , $url ] = explode ( ':' , $value , 3 );
$custom_gits [ $source_name ] = [ $branch , $url ];
}
2023-07-28 00:02:49 +08:00
2025-08-06 20:45:16 +08:00
// If passing --prefer-pre-built option, we need to load pre-built library list from pre-built.json targeted releases
if ( $this -> getOption ( 'prefer-pre-built' )) {
$repo = Config :: getPreBuilt ( 'repo' );
$pre_built_libs = Downloader :: getLatestGithubRelease ( $repo , [
'repo' => $repo ,
'prefer-stable' => Config :: getPreBuilt ( 'prefer-stable' ),
], false );
} else {
$pre_built_libs = [];
}
2024-07-07 20:45:18 +08:00
2025-08-06 20:45:16 +08:00
// Download them
f_mkdir ( DOWNLOAD_PATH );
$cnt = count ( $chosen_sources );
$ni = 0 ;
foreach ( $chosen_sources as $source ) {
++ $ni ;
if ( isset ( $custom_urls [ $source ])) {
$config = Config :: getSource ( $source );
$new_config = [
'type' => 'url' ,
'url' => $custom_urls [ $source ],
];
if ( isset ( $config [ 'path' ])) {
$new_config [ 'path' ] = $config [ 'path' ];
}
if ( isset ( $config [ 'filename' ])) {
$new_config [ 'filename' ] = $config [ 'filename' ];
}
logger () -> info ( " [ { $ni } / { $cnt } ] Downloading source { $source } from custom url: { $new_config [ 'url' ] } " );
Downloader :: downloadSource ( $source , $new_config , true );
} elseif ( isset ( $custom_gits [ $source ])) {
$config = Config :: getSource ( $source );
$new_config = [
'type' => 'git' ,
'rev' => $custom_gits [ $source ][ 0 ],
'url' => $custom_gits [ $source ][ 1 ],
];
if ( isset ( $config [ 'path' ])) {
$new_config [ 'path' ] = $config [ 'path' ];
}
logger () -> info ( " [ { $ni } / { $cnt } ] Downloading source { $source } from custom git: { $new_config [ 'url' ] } " );
Downloader :: downloadSource ( $source , $new_config , true );
} else {
2026-01-01 13:56:01 +01:00
// Handle version-specific php-src (php-src-8.2, php-src-8.3, etc.)
if ( preg_match ( '/^php-src-[\d.]+$/' , $source )) {
$config = Config :: getSource ( 'php-src' );
if ( $config === null ) {
logger () -> error ( 'php-src configuration not found in source.json' );
return static :: FAILURE ;
}
} else {
$config = Config :: getSource ( $source );
}
2025-08-06 20:45:16 +08:00
// Prefer pre-built, we need to search pre-built library
if ( $this -> getOption ( 'prefer-pre-built' ) && ( $config [ 'provide-pre-built' ] ? ? false ) === true ) {
// We need to replace pattern
$replace = [
'{name}' => $source ,
'{arch}' => arch2gnu ( php_uname ( 'm' )),
'{os}' => strtolower ( PHP_OS_FAMILY ),
'{libc}' => SPCTarget :: getLibc () ? ? 'default' ,
'{libcver}' => SPCTarget :: getLibcVersion () ? ? 'default' ,
2024-09-05 00:00:58 +08:00
];
2025-08-06 20:45:16 +08:00
$find = str_replace ( array_keys ( $replace ), array_values ( $replace ), Config :: getPreBuilt ( 'match-pattern' ));
// find filename in asset list
if (( $url = $this -> findPreBuilt ( $pre_built_libs , $find )) !== null ) {
logger () -> info ( " [ { $ni } / { $cnt } ] Downloading pre-built content { $source } " );
Downloader :: downloadSource ( $source , [ 'type' => 'url' , 'url' => $url ], $force_all || in_array ( $source , $force_list ), SPC_DOWNLOAD_PRE_BUILT );
continue ;
2024-09-05 00:00:58 +08:00
}
2025-08-06 20:45:16 +08:00
logger () -> warning ( " Pre-built content not found for { $source } , fallback to source download " );
}
logger () -> info ( " [ { $ni } / { $cnt } ] Downloading source { $source } " );
try {
Downloader :: downloadSource ( $source , $config , $force_all || in_array ( $source , $force_list ));
} catch ( SPCException $e ) {
// if `--no-alt` option is set, we will not download alternative sources
if ( $this -> getOption ( 'no-alt' )) {
throw $e ;
2024-07-07 20:45:18 +08:00
}
2025-08-06 20:45:16 +08:00
// if download failed, we will try to download alternative sources
logger () -> warning ( " Download failed: { $e -> getMessage () } " );
$alt_sources = Config :: getSource ( $source )[ 'alt' ] ? ? null ;
if ( $alt_sources === null ) {
logger () -> warning ( " No alternative sources found for { $source } , using default alternative source " );
$alt_config = array_merge ( $config , Downloader :: getDefaultAlternativeSource ( $source ));
} elseif ( $alt_sources === false ) {
throw new DownloaderException ( " No alternative sources found for { $source } , skipping alternative download " );
} else {
logger () -> notice ( " Trying to download alternative sources for { $source } " );
$alt_config = array_merge ( $config , $alt_sources );
2025-04-22 20:25:44 +08:00
}
2025-08-06 20:45:16 +08:00
Downloader :: downloadSource ( $source , $alt_config , $force_all || in_array ( $source , $force_list ));
2023-07-28 00:02:49 +08:00
}
}
2023-04-30 12:42:19 +08:00
}
2025-08-06 20:45:16 +08:00
$time = round ( microtime ( true ) - START_TIME , 3 );
logger () -> info ( 'Download complete, used ' . $time . ' s !' );
return static :: SUCCESS ;
2023-04-30 12:42:19 +08:00
}
2023-10-22 12:37:13 +08:00
private function downloadFromZip ( string $path ) : int
{
if ( ! file_exists ( $path )) {
logger () -> critical ( 'File ' . $path . ' not exist or not a zip archive.' );
return static :: FAILURE ;
}
// remove old download files first
if ( is_dir ( DOWNLOAD_PATH )) {
logger () -> warning ( 'You are doing some operations that not recoverable: removing directories below' );
logger () -> warning ( DOWNLOAD_PATH );
logger () -> alert ( 'I will remove these dir after 5 seconds !' );
sleep ( 5 );
f_passthru (( PHP_OS_FAMILY === 'Windows' ? 'rmdir /s /q ' : 'rm -rf ' ) . DOWNLOAD_PATH );
}
// unzip command check
2025-09-21 19:59:11 +02:00
if ( PHP_OS_FAMILY !== 'Windows' && ! self :: findCommand ( 'unzip' )) {
2025-08-06 20:45:16 +08:00
$this -> output -> writeln ( 'Missing unzip command, you need to install it first !' );
$this -> output -> writeln ( 'You can use "bin/spc doctor" command to check and install required tools' );
2023-10-22 12:37:13 +08:00
return static :: FAILURE ;
}
// create downloads
2025-08-06 20:45:16 +08:00
if ( PHP_OS_FAMILY === 'Windows' ) {
// Windows TODO
$this -> output -> writeln ( '<error>Windows currently does not support --from-zip !</error>' );
return static :: FAILURE ;
}
$abs_path = realpath ( $path );
f_passthru ( 'mkdir ' . DOWNLOAD_PATH . ' && cd ' . DOWNLOAD_PATH . ' && unzip ' . escapeshellarg ( $abs_path ));
2023-10-22 12:37:13 +08:00
2025-08-06 20:45:16 +08:00
if ( ! file_exists ( LockFile :: LOCK_FILE )) {
$this -> output -> writeln ( '<error>.lock.json not exist in "downloads/", please run "bin/spc download" first !</error>' );
2023-10-22 12:37:13 +08:00
return static :: FAILURE ;
}
2025-08-06 20:45:16 +08:00
$this -> output -> writeln ( '<info>Extract success</info>' );
2023-10-22 12:37:13 +08:00
return static :: SUCCESS ;
}
2023-10-22 16:35:15 +08:00
/**
* Calculate the sources by extensions
*
2025-08-06 20:17:26 +08:00
* @ param array $extensions extension list
* @ param bool $include_suggests include suggested libs and extensions ( default : true )
2023-10-22 16:35:15 +08:00
*/
private function calculateSourcesByExt ( array $extensions , bool $include_suggests = true ) : array
{
2024-02-16 18:56:33 +08:00
[ $extensions , $libraries ] = $include_suggests ? DependencyUtil :: getExtsAndLibs ( $extensions , [], true , true ) : DependencyUtil :: getExtsAndLibs ( $extensions );
2023-10-22 16:35:15 +08:00
$sources = [];
foreach ( $extensions as $extension ) {
if ( Config :: getExt ( $extension , 'type' ) === 'external' ) {
$sources [] = Config :: getExt ( $extension , 'source' );
}
}
foreach ( $libraries as $library ) {
2025-03-08 14:29:44 +08:00
$source = Config :: getLib ( $library , 'source' );
if ( $source !== null ) {
$sources [] = $source ;
}
2023-10-22 16:35:15 +08:00
}
return array_values ( array_unique ( $sources ));
}
2024-03-05 21:43:09 +08:00
/**
* Calculate the sources by libraries
*
2025-08-06 20:17:26 +08:00
* @ param array $libs library list
* @ param bool $include_suggests include suggested libs ( default : true )
2024-03-05 21:43:09 +08:00
*/
private function calculateSourcesByLib ( array $libs , bool $include_suggests = true ) : array
{
$libs = DependencyUtil :: getLibs ( $libs , $include_suggests );
$sources = [];
foreach ( $libs as $library ) {
$sources [] = Config :: getLib ( $library , 'source' );
}
return array_values ( array_unique ( $sources ));
}
2024-07-07 20:45:18 +08:00
/**
* @ param array $assets Asset list from GitHub API
* @ param string $filename Match file name , e . g . pkg - config - aarch64 - darwin . txz
* @ return null | string Return the download URL if found , otherwise null
*/
private function findPreBuilt ( array $assets , string $filename ) : ? string
{
2025-03-30 20:21:56 +08:00
logger () -> debug ( " Finding pre-built asset { $filename } " );
2024-07-07 20:45:18 +08:00
foreach ( $assets as $asset ) {
if ( $asset [ 'name' ] === $filename ) {
return $asset [ 'browser_download_url' ];
}
}
return null ;
}
2025-04-22 20:25:44 +08:00
private function _clean () : int
{
logger () -> warning ( 'You are doing some operations that not recoverable: removing directories below' );
logger () -> warning ( SOURCE_PATH );
logger () -> warning ( DOWNLOAD_PATH );
logger () -> warning ( BUILD_ROOT_PATH );
logger () -> alert ( 'I will remove these dir after 5 seconds !' );
sleep ( 5 );
if ( PHP_OS_FAMILY === 'Windows' ) {
f_passthru ( 'rmdir /s /q ' . SOURCE_PATH );
f_passthru ( 'rmdir /s /q ' . DOWNLOAD_PATH );
f_passthru ( 'rmdir /s /q ' . BUILD_ROOT_PATH );
} else {
f_passthru ( 'rm -rf ' . SOURCE_PATH . '/*' );
f_passthru ( 'rm -rf ' . DOWNLOAD_PATH . '/*' );
f_passthru ( 'rm -rf ' . BUILD_ROOT_PATH . '/*' );
}
return static :: FAILURE ;
}
2026-01-01 14:31:35 +01:00
private function handleUpdate () : int
{
logger () -> info ( 'Checking sources for updates...' );
// Get lock file content
$lock_file_path = LockFile :: LOCK_FILE ;
if ( ! file_exists ( $lock_file_path )) {
logger () -> warning ( 'No lock file found. Please download sources first using "bin/spc download"' );
return static :: FAILURE ;
}
$lock_content = json_decode ( file_get_contents ( $lock_file_path ), true );
if ( $lock_content === null || ! is_array ( $lock_content )) {
logger () -> error ( 'Failed to parse lock file' );
return static :: FAILURE ;
}
// Filter sources to check
$sources_arg = $this -> getArgument ( 'sources' );
if ( ! empty ( $sources_arg )) {
$requested_sources = array_map ( 'trim' , array_filter ( explode ( ',' , $sources_arg )));
$sources_to_check = [];
foreach ( $requested_sources as $source ) {
if ( isset ( $lock_content [ $source ])) {
$sources_to_check [ $source ] = $lock_content [ $source ];
} else {
logger () -> warning ( " Source ' { $source } ' not found in lock file, skipping " );
}
}
} else {
$sources_to_check = $lock_content ;
}
// Filter out pre-built sources (they are derivatives)
$sources_to_check = array_filter ( $sources_to_check , function ( $lock_item , $name ) {
// Skip pre-built sources (they contain OS/arch in the name)
if ( str_contains ( $name , '-Linux-' ) || str_contains ( $name , '-Windows-' ) || str_contains ( $name , '-Darwin-' )) {
logger () -> debug ( " Skipping pre-built source: { $name } " );
return false ;
}
return true ;
}, ARRAY_FILTER_USE_BOTH );
if ( empty ( $sources_to_check )) {
logger () -> warning ( 'No sources to check' );
return static :: FAILURE ;
}
$total = count ( $sources_to_check );
$current = 0 ;
$updated_sources = [];
foreach ( $sources_to_check as $name => $lock_item ) {
++ $current ;
try {
// Handle version-specific php-src (php-src-8.2, php-src-8.3, etc.)
if ( preg_match ( '/^php-src-[\d.]+$/' , $name )) {
$config = Config :: getSource ( 'php-src' );
} else {
$config = Config :: getSource ( $name );
}
if ( $config === null ) {
logger () -> warning ( " [ { $current } / { $total } ] Source ' { $name } ' not found in source config, skipping " );
continue ;
}
// Check and update based on source type
$source_type = $lock_item [ 'source_type' ] ? ? 'unknown' ;
if ( $source_type === SPC_SOURCE_ARCHIVE ) {
if ( $this -> checkArchiveSourceUpdate ( $name , $lock_item , $config , $current , $total )) {
$updated_sources [] = $name ;
}
} elseif ( $source_type === SPC_SOURCE_GIT ) {
if ( $this -> checkGitSourceUpdate ( $name , $lock_item , $config , $current , $total )) {
$updated_sources [] = $name ;
}
} elseif ( $source_type === SPC_SOURCE_LOCAL ) {
logger () -> debug ( " [ { $current } / { $total } ] Source ' { $name } ' is local, skipping " );
} else {
logger () -> warning ( " [ { $current } / { $total } ] Unknown source type ' { $source_type } ' for ' { $name } ', skipping " );
}
} catch ( \Throwable $e ) {
logger () -> error ( " [ { $current } / { $total } ] Error checking ' { $name } ': { $e -> getMessage () } " );
continue ;
}
}
// Output summary
if ( empty ( $updated_sources )) {
logger () -> info ( 'All sources are up to date.' );
} else {
logger () -> info ( 'Updated sources: ' . implode ( ', ' , $updated_sources ));
// Write updated sources to file
$date = date ( 'Y-m-d' );
$update_file = DOWNLOAD_PATH . '/.update-' . $date . '.txt' ;
$content = implode ( ',' , $updated_sources );
file_put_contents ( $update_file , $content );
logger () -> debug ( " Updated sources written to: { $update_file } " );
}
return static :: SUCCESS ;
}
private function checkCustomSourceUpdate ( string $name , array $lock , array $config , int $current , int $total ) : bool
{
$classes = FileSystem :: getClassesPsr4 ( ROOT_DIR . '/src/SPC/store/source' , 'SPC\store\source' );
foreach ( $classes as $class ) {
// Support php-src and php-src-X.Y patterns
$matches = ( $class :: NAME === $name ) ||
( $class :: NAME === 'php-src' && preg_match ( '/^php-src(-[\d.]+)?$/' , $name ));
if ( is_a ( $class , CustomSourceBase :: class , true ) && $matches ) {
try {
$config [ 'source_name' ] = $name ;
$updated = ( new $class ()) -> update ( $lock , $config );
if ( $updated ) {
logger () -> info ( " [ { $current } / { $total } ] Source ' { $name } ' updated " );
} else {
logger () -> info ( " [ { $current } / { $total } ] Source ' { $name } ' is up to date " );
}
return $updated ;
} catch ( \Throwable $e ) {
logger () -> warning ( " [ { $current } / { $total } ] Failed to check ' { $name } ': { $e -> getMessage () } " );
return false ;
}
}
}
logger () -> warning ( " [ { $current } / { $total } ] Custom source handler for ' { $name } ' not found " );
return false ;
}
/**
* Check and update an archive source
*
* @ param string $name Source name
* @ param array $lock Lock file entry
* @ param array $config Source configuration
* @ param int $current Current progress number
* @ param int $total Total sources to check
* @ return bool True if updated , false otherwise
*/
private function checkArchiveSourceUpdate ( string $name , array $lock , array $config , int $current , int $total ) : bool
{
$type = $config [ 'type' ] ? ? 'unknown' ;
$locked_filename = $lock [ 'filename' ] ? ? '' ;
// Skip local types that don't support version detection
if ( in_array ( $type , [ 'url' , 'local' , 'unknown' ])) {
logger () -> debug ( " [ { $current } / { $total } ] Source ' { $name } ' (type: { $type } ) doesn't support version detection, skipping " );
return false ;
}
try {
// Get latest version info
$latest_info = match ( $type ) {
'ghtar' => Downloader :: getLatestGithubTarball ( $name , $config ),
'ghtagtar' => Downloader :: getLatestGithubTarball ( $name , $config , 'tags' ),
'ghrel' => Downloader :: getLatestGithubRelease ( $name , $config ),
'pie' => Downloader :: getPIEInfo ( $name , $config ),
'bitbuckettag' => Downloader :: getLatestBitbucketTag ( $name , $config ),
'filelist' => Downloader :: getFromFileList ( $name , $config ),
'url' => Downloader :: getLatestUrlInfo ( $name , $config ),
'custom' => $this -> checkCustomSourceUpdate ( $name , $lock , $config , $current , $total ),
default => null ,
};
if ( $latest_info === null ) {
logger () -> warning ( " [ { $current } / { $total } ] Could not get version info for ' { $name } ' (type: { $type } ) " );
return false ;
}
$latest_filename = $latest_info [ 1 ] ? ? '' ;
// Compare filenames
if ( $locked_filename !== $latest_filename ) {
logger () -> info ( " [ { $current } / { $total } ] Update available for ' { $name } ': { $locked_filename } → { $latest_filename } " );
$this -> downloadSourceForUpdate ( $name , $config , $current , $total );
return true ;
}
logger () -> info ( " [ { $current } / { $total } ] Source ' { $name } ' is up to date " );
return false ;
} catch ( DownloaderException $e ) {
logger () -> warning ( " [ { $current } / { $total } ] Failed to check ' { $name } ': { $e -> getMessage () } " );
return false ;
}
}
/**
* Check and update a git source
*
* @ param string $name Source name
* @ param array $lock Lock file entry
* @ param array $config Source configuration
* @ param int $current Current progress number
* @ param int $total Total sources to check
* @ return bool True if updated , false otherwise
*/
private function checkGitSourceUpdate ( string $name , array $lock , array $config , int $current , int $total ) : bool
{
$locked_hash = $lock [ 'hash' ] ? ? '' ;
$url = $config [ 'url' ] ? ? '' ;
$branch = $config [ 'rev' ] ? ? 'main' ;
if ( empty ( $url )) {
logger () -> warning ( " [ { $current } / { $total } ] No URL found for git source ' { $name } ' " );
return false ;
}
try {
$remote_hash = $this -> getRemoteGitCommit ( $url , $branch );
if ( $remote_hash === null ) {
logger () -> warning ( " [ { $current } / { $total } ] Could not fetch remote commit for ' { $name } ' " );
return false ;
}
// Compare hashes (use first 7 chars for display)
$locked_short = substr ( $locked_hash , 0 , 7 );
$remote_short = substr ( $remote_hash , 0 , 7 );
if ( $locked_hash !== $remote_hash ) {
logger () -> info ( " [ { $current } / { $total } ] Update available for ' { $name } ': { $locked_short } → { $remote_short } " );
$this -> downloadSourceForUpdate ( $name , $config , $current , $total );
return true ;
}
logger () -> info ( " [ { $current } / { $total } ] Source ' { $name } ' is up to date " );
return false ;
} catch ( \Throwable $e ) {
logger () -> warning ( " [ { $current } / { $total } ] Failed to check ' { $name } ': { $e -> getMessage () } " );
return false ;
}
}
/**
* Download a source after removing old lock entry
*
* @ param string $name Source name
* @ param array $config Source configuration
* @ param int $current Current progress number
* @ param int $total Total sources to check
*/
private function downloadSourceForUpdate ( string $name , array $config , int $current , int $total ) : void
{
logger () -> info ( " [ { $current } / { $total } ] Downloading ' { $name } '... " );
// Remove old lock entry (this triggers cleanup of old files)
LockFile :: put ( $name , null );
// Download new version
Downloader :: downloadSource ( $name , $config , true );
}
/**
* Get remote git commit hash without cloning
*
* @ param string $url Git repository URL
* @ param string $branch Branch or tag to check
* @ return null | string Remote commit hash or null on failure
*/
private function getRemoteGitCommit ( string $url , string $branch ) : ? string
{
try {
$cmd = SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg ( $url ) . ' ' . escapeshellarg ( $branch );
f_exec ( $cmd , $output , $ret );
if ( $ret !== 0 || empty ( $output )) {
return null ;
}
// Output format: "commit_hash\trefs/heads/branch" or "commit_hash\tHEAD"
$parts = preg_split ( '/\s+/' , $output [ 0 ]);
return $parts [ 0 ] ? ? null ;
} catch ( \Throwable $e ) {
logger () -> debug ( " Failed to fetch remote git commit: { $e -> getMessage () } " );
return null ;
}
}
2023-04-30 12:42:19 +08:00
}