Files
static-php-cli/src/StaticPHP/Runtime/Shell/DefaultShell.php

216 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace StaticPHP\Runtime\Shell;
use StaticPHP\Exception\InterruptException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Runtime\SystemTarget;
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.
*/
public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string
{
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}" : '';
$compressed_arg = $compressed ? '--compressed' : '';
$cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}";
$this->logCommandInfo($cmd);
logger()->debug("[CURL EXECUTE] {$cmd}");
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
$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';
$cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}");
$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, int $retries = 0): 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' : '');
$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}");
$this->logCommandInfo($cmd);
logger()->debug("[GIT CLONE] {$cmd}");
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;
}
if ($submodules !== null) {
$depth_flag = $shallow ? '--depth 1' : '';
foreach ($submodules as $submodule) {
$submodule = escapeshellarg($submodule);
$submodule_cmd = clean_spaces("{$git} submodule update --init {$depth_flag} {$submodule}");
$this->logCommandInfo($submodule_cmd);
logger()->debug("[GIT SUBMODULE] {$submodule_cmd}");
$this->passthru($submodule_cmd, $this->console_putput, cwd: $path);
}
}
}
/**
* 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)
*/
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): bool
{
$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';
$tar = SystemTarget::isUnix() ? 'tar' : '"C:\Windows\system32\tar.exe"';
$cmd = "{$tar} {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}";
$this->logCommandInfo($cmd);
logger()->debug("[TAR EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput);
return true;
}
/**
* 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}");
$this->passthru($cmd, $this->console_putput);
}
/**
* 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
*/
public function execute7zExtract(string $archive_path, string $target_path): bool
{
$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';
$run = function ($cmd) {
$this->logCommandInfo($cmd);
logger()->debug("[7Z EXTRACT] {$cmd}");
$this->passthru($cmd, $this->console_putput);
};
$extname = FileSystem::extname($archive_path);
$tar = SystemTarget::isUnix() ? 'tar' : '"C:\Windows\system32\tar.exe"';
match ($extname) {
'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'),
'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | {$tar} -f - -x -C {$target_arg} --strip-components 1"),
default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"),
};
return true;
}
}