2025-11-30 15:35:04 +08:00
< ? php
declare ( strict_types = 1 );
namespace StaticPHP\Runtime\Shell ;
2026-06-14 11:51:07 +07:00
use StaticPHP\Exception\ExecutionException ;
2025-11-30 15:35:04 +08:00
use StaticPHP\Exception\InterruptException ;
use StaticPHP\Exception\SPCInternalException ;
2026-03-24 13:30:17 +08:00
use StaticPHP\Runtime\SystemTarget ;
2025-11-30 15:35:04 +08:00
use StaticPHP\Util\FileSystem ;
/**
* A default shell implementation that does not support custom commands .
* Used as a internal command caller when some place needs os - irrelevant shell .
*/
class DefaultShell extends Shell
{
/**
* @ internal
*/
public function exec ( string $cmd ) : static
{
throw new SPCInternalException ( 'DefaultShell does not support custom command execution.' );
}
/**
* Execute a cURL command to fetch data from a URL .
*/
2026-02-28 17:07:24 +08:00
public function executeCurl ( string $url , string $method = 'GET' , array $headers = [], array $hooks = [], int $retries = 0 , bool $compressed = false ) : false | string
2025-11-30 15:35:04 +08:00
{
foreach ( $hooks as $hook ) {
$hook ( $method , $url , $headers );
}
$url_arg = escapeshellarg ( $url );
$method_arg = match ( $method ) {
'GET' => '' ,
'HEAD' => '-I' ,
default => " -X { $method } " ,
};
$header_arg = implode ( ' ' , array_map ( fn ( $v ) => '"-H' . $v . '"' , $headers ));
$retry_arg = $retries > 0 ? " --retry { $retries } " : '' ;
2026-02-28 17:07:24 +08:00
$compressed_arg = $compressed ? '--compressed' : '' ;
$cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 { $retry_arg } { $compressed_arg } { $method_arg } { $header_arg } { $url_arg } " ;
2025-11-30 15:35:04 +08:00
$this -> logCommandInfo ( $cmd );
2026-05-09 10:19:16 +08:00
logger () -> debug ( " [CURL EXECUTE] { $cmd } " );
2025-12-11 11:35:12 +08:00
$result = $this -> passthru ( $cmd , capture_output : true , throw_on_error : false );
2025-11-30 15:35:04 +08:00
$ret = $result [ 'code' ];
$output = $result [ 'output' ];
if ( $ret !== 0 ) {
logger () -> debug ( " [CURL ERROR] Command exited with code: { $ret } " );
}
if ( $ret === 2 || $ret === - 1073741510 ) {
throw new InterruptException ( sprintf ( 'Canceled fetching "%s"' , $url ));
}
if ( $ret !== 0 ) {
return false ;
}
return trim ( $output );
}
/**
* Execute a cURL command to download a file from a URL .
*/
public function executeCurlDownload ( string $url , string $path , array $headers = [], array $hooks = [], int $retries = 0 ) : void
{
foreach ( $hooks as $hook ) {
$hook ( 'GET' , $url , $headers );
}
$url_arg = escapeshellarg ( $url );
$path_arg = escapeshellarg ( $path );
$header_arg = implode ( ' ' , array_map ( fn ( $v ) => '"-H' . $v . '"' , $headers ));
$retry_arg = $retries > 0 ? " --retry { $retries } " : '' ;
$check = $this -> console_putput ? '#' : 's' ;
2026-02-28 17:07:24 +08:00
$cmd = clean_spaces ( SPC_CURL_EXEC . " - { $check } fSL --max-time 3600 { $retry_arg } { $header_arg } -o { $path_arg } { $url_arg } " );
2025-11-30 15:35:04 +08:00
$this -> logCommandInfo ( $cmd );
logger () -> debug ( '[CURL DOWNLOAD] ' . $cmd );
$this -> passthru ( $cmd , $this -> console_putput , capture_output : false , throw_on_error : true );
}
/**
* Execute a Git clone command to clone a repository .
*/
2026-05-09 14:13:47 +08:00
public function executeGitClone ( string $url , string $branch , string $path , bool $shallow = true , ? array $submodules = null , int $retries = 0 ) : void
2025-11-30 15:35:04 +08:00
{
$path = FileSystem :: convertPath ( $path );
if ( file_exists ( $path )) {
FileSystem :: removeDir ( $path );
}
$git = SPC_GIT_EXEC ;
$url_arg = escapeshellarg ( $url );
$branch_arg = escapeshellarg ( $branch );
$path_arg = escapeshellarg ( $path );
$shallow_arg = $shallow ? '--depth 1 --single-branch' : '' ;
$submodules_arg = ( $submodules === null && $shallow ) ? '--recursive --shallow-submodules' : ( $submodules === null ? '--recursive' : '' );
2026-02-28 17:07:24 +08:00
$cmd = clean_spaces ( " { $git } clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch { $branch_arg } { $shallow_arg } { $submodules_arg } { $url_arg } { $path_arg } " );
2025-11-30 15:35:04 +08:00
$this -> logCommandInfo ( $cmd );
logger () -> debug ( " [GIT CLONE] { $cmd } " );
2026-05-09 14:13:47 +08:00
try {
$this -> passthru ( $cmd , $this -> console_putput );
} catch ( InterruptException $e ) {
throw $e ;
} catch ( \Throwable $e ) {
if ( $retries > 0 ) {
logger () -> warning ( " Git clone failed, retrying... ( { $retries } retries left) " );
if ( is_dir ( $path )) {
FileSystem :: removeDir ( $path );
}
$this -> executeGitClone ( $url , $branch , $path , $shallow , $submodules , $retries - 1 );
return ;
}
throw $e ;
}
2025-11-30 15:35:04 +08:00
if ( $submodules !== null ) {
$depth_flag = $shallow ? '--depth 1' : '' ;
foreach ( $submodules as $submodule ) {
$submodule = escapeshellarg ( $submodule );
2025-12-11 11:35:12 +08:00
$submodule_cmd = clean_spaces ( " { $git } submodule update --init { $depth_flag } { $submodule } " );
2025-11-30 15:35:04 +08:00
$this -> logCommandInfo ( $submodule_cmd );
logger () -> debug ( " [GIT SUBMODULE] { $submodule_cmd } " );
2026-02-05 20:56:50 +08:00
$this -> passthru ( $submodule_cmd , $this -> console_putput , cwd : $path );
2025-11-30 15:35:04 +08:00
}
}
}
/**
* Execute a tar command to extract an archive .
*
* @ param string $archive_path Path to the archive file
* @ param string $target_path Path to extract to
* @ param string $compression Compression type : 'gz' , 'bz2' , 'xz' , or 'none'
* @ param int $strip Number of leading components to strip ( default : 1 )
*/
2025-12-11 11:35:12 +08:00
public function executeTarExtract ( string $archive_path , string $target_path , string $compression , int $strip = 1 ) : bool
2025-11-30 15:35:04 +08:00
{
$archive_arg = escapeshellarg ( FileSystem :: convertPath ( $archive_path ));
$target_arg = escapeshellarg ( FileSystem :: convertPath ( $target_path ));
$compression_flag = match ( $compression ) {
'gz' => '-z' ,
'bz2' => '-j' ,
'xz' => '-J' ,
'none' => '' ,
default => throw new SPCInternalException ( " Unknown compression type: { $compression } " ),
};
$mute = $this -> console_putput ? '' : ' 2>/dev/null' ;
2026-03-31 15:10:47 +08:00
$tar = SystemTarget :: isUnix () ? 'tar' : '"C:\Windows\system32\tar.exe"' ;
2026-03-24 13:30:17 +08:00
$cmd = " { $tar } { $compression_flag } xf { $archive_arg } --strip-components { $strip } -C { $target_arg } { $mute } " ;
2025-11-30 15:35:04 +08:00
$this -> logCommandInfo ( $cmd );
logger () -> debug ( " [TAR EXTRACT] { $cmd } " );
2026-06-14 11:51:07 +07:00
$this -> passthruTolerateSymlinks ( $cmd );
2025-12-11 11:35:12 +08:00
return true ;
2025-11-30 15:35:04 +08:00
}
/**
* Execute an unzip command to extract a zip archive .
*
* @ param string $zip_path Path to the zip file
* @ param string $target_path Path to extract to
*/
public function executeUnzip ( string $zip_path , string $target_path ) : void
{
$zip_arg = escapeshellarg ( FileSystem :: convertPath ( $zip_path ));
$target_arg = escapeshellarg ( FileSystem :: convertPath ( $target_path ));
$mute = $this -> console_putput ? '' : ' > /dev/null' ;
$cmd = " unzip { $zip_arg } -d { $target_arg } { $mute } " ;
$this -> logCommandInfo ( $cmd );
logger () -> debug ( " [UNZIP] { $cmd } " );
2025-12-11 11:35:12 +08:00
$this -> passthru ( $cmd , $this -> console_putput );
2025-11-30 15:35:04 +08:00
}
/**
* Execute a 7 za command to extract an archive ( Windows ) .
*
* @ param string $archive_path Path to the archive file
* @ param string $target_path Path to extract to
*/
2025-12-11 11:35:12 +08:00
public function execute7zExtract ( string $archive_path , string $target_path ) : bool
2025-11-30 15:35:04 +08:00
{
feat(windows): replace php-sdk-binary-tools with MSYS2 + 7za-win
- Add msys2-build-essentials target: downloads the MSYS2 nightly sfx,
extracts it, disables PGP keyring (CI-safe), runs two-pass pacman
update, and installs autotools build essentials (make, autoconf,
automake, libtool, pkgconf, perl).
- Add 7za-win target: downloads 7za.exe to PKG_ROOT_PATH\bin.
- Remove php-sdk-binary-tools target and all PHP_SDK_PATH references;
replace with SPC_MSYS2_PATH throughout Artifact, ArtifactExtractor,
FileSystem, DefaultShell and MSVCToolchain.
- Replace {php_sdk_path} path placeholder with {spc_msys2_path}.
- WindowsToolCheck: replace checkSDK/installSDK with checkMsys2,
installMsys2 and check7zaWin/install7zaWin fix items.
- nasm.yml: extract nasm.exe/ndisasm.exe to {pkg_root_path}/bin.
- env.ini: rename PHP_SDK_PATH to SPC_MSYS2_PATH.
- Docs: update Windows migration guide and package-model placeholder docs.
2026-06-21 16:56:40 +08:00
// 7za.exe is installed by the 7za-win target package into PKG_ROOT_PATH\bin,
// which is added to PATH by MSVCToolchain::initEnv().
$_7z_path = FileSystem :: convertPath ( PKG_ROOT_PATH . '\bin\7za.exe' );
if ( ! file_exists ( $_7z_path )) {
throw new SPCInternalException ( '7za.exe not found. Please install the 7za-win target package.' );
2025-11-30 15:35:04 +08:00
}
feat(windows): replace php-sdk-binary-tools with MSYS2 + 7za-win
- Add msys2-build-essentials target: downloads the MSYS2 nightly sfx,
extracts it, disables PGP keyring (CI-safe), runs two-pass pacman
update, and installs autotools build essentials (make, autoconf,
automake, libtool, pkgconf, perl).
- Add 7za-win target: downloads 7za.exe to PKG_ROOT_PATH\bin.
- Remove php-sdk-binary-tools target and all PHP_SDK_PATH references;
replace with SPC_MSYS2_PATH throughout Artifact, ArtifactExtractor,
FileSystem, DefaultShell and MSVCToolchain.
- Replace {php_sdk_path} path placeholder with {spc_msys2_path}.
- WindowsToolCheck: replace checkSDK/installSDK with checkMsys2,
installMsys2 and check7zaWin/install7zaWin fix items.
- nasm.yml: extract nasm.exe/ndisasm.exe to {pkg_root_path}/bin.
- env.ini: rename PHP_SDK_PATH to SPC_MSYS2_PATH.
- Docs: update Windows migration guide and package-model placeholder docs.
2026-06-21 16:56:40 +08:00
$_7z = escapeshellarg ( FileSystem :: convertPath ( $_7z_path ));
2025-11-30 15:35:04 +08:00
$archive_arg = escapeshellarg ( FileSystem :: convertPath ( $archive_path ));
$target_arg = escapeshellarg ( FileSystem :: convertPath ( $target_path ));
$mute = $this -> console_putput ? '' : ' > NUL' ;
2025-12-11 11:35:12 +08:00
$run = function ( $cmd ) {
$this -> logCommandInfo ( $cmd );
logger () -> debug ( " [7Z EXTRACT] { $cmd } " );
2026-06-14 11:51:07 +07:00
$this -> passthruTolerateSymlinks ( $cmd );
2025-12-11 11:35:12 +08:00
};
2025-11-30 15:35:04 +08:00
2025-12-11 11:35:12 +08:00
$extname = FileSystem :: extname ( $archive_path );
2026-03-31 15:10:47 +08:00
$tar = SystemTarget :: isUnix () ? 'tar' : '"C:\Windows\system32\tar.exe"' ;
2026-03-24 13:30:17 +08:00
2025-12-11 11:35:12 +08:00
match ( $extname ) {
'tar' => $this -> executeTarExtract ( $archive_path , $target_path , 'none' ),
2026-03-24 13:30:17 +08:00
'gz' , 'tgz' , 'xz' , 'txz' , 'bz2' => $run ( " { $_7z } x -so { $archive_arg } | { $tar } -f - -x -C { $target_arg } --strip-components 1 " ),
2025-12-11 11:35:12 +08:00
default => $run ( " { $_7z } x { $archive_arg } -o { $target_arg } -y { $mute } " ),
};
return true ;
2025-11-30 15:35:04 +08:00
}
2026-06-14 11:51:07 +07:00
/**
* Run an extraction command , tolerating symbolic links that the host cannot create .
*
* Windows tar . exe ( bsdtar ) cannot create the symbolic links some archives ship ( e . g . zstd ' s
* tests / cli - tests / bin / unzstd -> zstd ), failing each with " Can't create '...': Invalid argument "
* and exiting non - zero . Those entries are never needed to build , so on Windows we swallow a
* failure whose only errors are such symlink creations and continue . Any other failure still throws .
*/
private function passthruTolerateSymlinks ( string $cmd ) : void
{
// Symlink creation only fails on a Windows host; elsewhere extraction handles symlinks fine.
if ( PHP_OS_FAMILY !== 'Windows' ) {
$this -> passthru ( $cmd , $this -> console_putput );
return ;
}
$result = $this -> passthru ( $cmd , $this -> console_putput , capture_output : true , throw_on_error : false );
if ( $result [ 'code' ] === 0 ) {
return ;
}
if ( $this -> isSymlinkOnlyExtractFailure ( $result [ 'output' ])) {
logger () -> warning ( 'Some symbolic links could not be created during extraction and were skipped (not supported on this Windows host). This is harmless for building.' );
return ;
}
throw new ExecutionException (
cmd : $cmd ,
message : " Command exited with non-zero code: { $result [ 'code' ] } " ,
code : $result [ 'code' ],
cd : $this -> cd ,
env : $this -> env ,
);
}
/**
* Decide whether an extraction failure was caused solely by symbolic links that could not be
* created on Windows . Returns true only when at least one such error is present and no other
* error - looking output is found , so genuine extraction failures still propagate .
*/
private function isSymlinkOnlyExtractFailure ( string $output ) : bool
{
$saw_symlink_error = false ;
foreach ( preg_split ( '/\r\n|\r|\n/' , $output ) ? : [] as $line ) {
$line = trim ( $line );
if ( $line === '' ) {
continue ;
}
// bsdtar's trailing summary line; not an error on its own.
if ( str_contains ( $line , 'Error exit delayed from previous errors' )) {
continue ;
}
// The symlink (or other unsupported special file) that Windows refused to create.
if ( str_contains ( $line , " Can't create " ) && str_contains ( $line , 'Invalid argument' )) {
$saw_symlink_error = true ;
continue ;
}
// Any other error-looking line means this was not a clean symlink-only failure.
if ( preg_match ( '/\berror\b|cannot|can\'t|failed|denied|no space|not permitted/i' , $line )) {
return false ;
}
}
return $saw_symlink_error ;
}
2025-11-30 15:35:04 +08:00
}