2025-11-30 15:35:04 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace StaticPHP\Runtime\Shell;
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void
|
|
|
|
|
{
|
|
|
|
|
$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}");
|
2025-12-11 11:35:12 +08:00
|
|
|
$this->passthru($cmd, $this->console_putput);
|
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}");
|
2025-12-11 11:35:12 +08:00
|
|
|
$this->passthru($cmd, $this->console_putput);
|
|
|
|
|
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 7za 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
|
|
|
{
|
|
|
|
|
$sdk_path = getenv('PHP_SDK_PATH');
|
|
|
|
|
if ($sdk_path === false) {
|
|
|
|
|
throw new SPCInternalException('PHP_SDK_PATH environment variable is not set');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$_7z = escapeshellarg(FileSystem::convertPath($sdk_path . '/bin/7za.exe'));
|
|
|
|
|
$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}");
|
|
|
|
|
$this->passthru($cmd, $this->console_putput);
|
|
|
|
|
};
|
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
|
|
|
}
|
|
|
|
|
}
|