mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-19 13:24:51 +08:00
Enhance Windows support by updating artifact configuration and improving extraction logic
This commit is contained in:
parent
dc05ad23c9
commit
2080407283
@ -11,7 +11,7 @@
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}",
|
||||
"action": ".\\vendor\\bin\\php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential",
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
|
||||
|
||||
@ -80,7 +80,11 @@
|
||||
},
|
||||
"strawberry-perl": {
|
||||
"binary": {
|
||||
"windows-x86_64": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip"
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip",
|
||||
"extract": "{pkg_root_path}/strawberry-perl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"upx": {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
{
|
||||
"vswhere": {
|
||||
"type": "target",
|
||||
"artifact": "vswhere"
|
||||
"artifact": "vswhere",
|
||||
"static-bins@windows": [
|
||||
"vswhere.exe"
|
||||
]
|
||||
},
|
||||
"pkg-config": {
|
||||
"type": "target",
|
||||
|
||||
@ -167,11 +167,22 @@ class Artifact
|
||||
return false;
|
||||
}
|
||||
|
||||
// For standalone mode, check directory and hash
|
||||
// For standalone mode, check directory or file and hash
|
||||
$target_path = $extract_config['path'];
|
||||
|
||||
if (!is_dir($target_path)) {
|
||||
return false;
|
||||
// Check if target is a file or directory
|
||||
$is_file_target = !is_dir($target_path) && str_contains($target_path, '.');
|
||||
|
||||
if ($is_file_target) {
|
||||
// For single file extraction (e.g., vswhere.exe)
|
||||
if (!file_exists($target_path)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// For directory extraction
|
||||
if (!is_dir($target_path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$compare_hash) {
|
||||
@ -320,7 +331,7 @@ class Artifact
|
||||
* For merge mode, returns the base path.
|
||||
* For standalone mode, returns the specific directory.
|
||||
*/
|
||||
public function getBinaryDir(): string
|
||||
public function getBinaryDir(): ?string
|
||||
{
|
||||
$config = $this->getBinaryExtractConfig();
|
||||
return $config['path'];
|
||||
|
||||
@ -293,7 +293,7 @@ class ArtifactExtractor
|
||||
// Process file mappings
|
||||
foreach ($file_map as $src_pattern => $dst_path) {
|
||||
$dst_path = $this->replacePathVariables($dst_path);
|
||||
$src_full = "{$temp_path}/{$src_pattern}";
|
||||
$src_full = FileSystem::convertPath("{$temp_path}/{$src_pattern}");
|
||||
|
||||
// Handle glob patterns
|
||||
if (str_contains($src_pattern, '*')) {
|
||||
@ -460,40 +460,36 @@ class ArtifactExtractor
|
||||
$target = FileSystem::convertPath($target);
|
||||
$filename = FileSystem::convertPath($filename);
|
||||
|
||||
FileSystem::createDir($target);
|
||||
$extname = FileSystem::extname($filename);
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
// Use 7za.exe for Windows
|
||||
$is_txz = str_ends_with($filename, '.txz') || str_ends_with($filename, '.tar.xz');
|
||||
default_shell()->execute7zExtract($filename, $target, $is_txz);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unix-like systems: determine compression type
|
||||
if (str_ends_with($filename, '.tar.gz') || str_ends_with($filename, '.tgz')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'gz');
|
||||
} elseif (str_ends_with($filename, '.tar.bz2')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'bz2');
|
||||
} elseif (str_ends_with($filename, '.tar.xz') || str_ends_with($filename, '.txz')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'xz');
|
||||
} elseif (str_ends_with($filename, '.tar')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'none');
|
||||
} elseif (str_ends_with($filename, '.zip')) {
|
||||
// Zip requires special handling for strip-components
|
||||
$this->unzipWithStrip($filename, $target);
|
||||
} elseif (str_ends_with($filename, '.exe')) {
|
||||
// exe just copy to target
|
||||
$dest_file = FileSystem::convertPath("{$target}/" . basename($filename));
|
||||
FileSystem::copy($filename, $dest_file);
|
||||
} else {
|
||||
throw new FileSystemException("Unknown archive format: {$filename}");
|
||||
if ($extname !== 'exe' && !is_dir($target)) {
|
||||
FileSystem::createDir($target);
|
||||
}
|
||||
match (SystemTarget::getTargetOS()) {
|
||||
'Windows' => match ($extname) {
|
||||
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
|
||||
'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target),
|
||||
'zip' => $this->unzipWithStrip($filename, $target),
|
||||
'exe' => $this->copyFile($filename, $target),
|
||||
default => throw new FileSystemException("Unknown archive format: {$filename}"),
|
||||
},
|
||||
'Linux', 'Darwin' => match ($extname) {
|
||||
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
|
||||
'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'),
|
||||
'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'),
|
||||
'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'),
|
||||
'zip' => $this->unzipWithStrip($filename, $target),
|
||||
'exe' => $this->copyFile($filename, $target),
|
||||
default => throw new FileSystemException("Unknown archive format: {$filename}"),
|
||||
},
|
||||
default => throw new SPCInternalException('Unsupported OS for archive extraction')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip file with stripping top-level directory.
|
||||
*/
|
||||
protected function unzipWithStrip(string $zip_file, string $extract_path): void
|
||||
protected function unzipWithStrip(string $zip_file, string $extract_path): bool
|
||||
{
|
||||
$temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16)));
|
||||
$zip_file = FileSystem::convertPath($zip_file);
|
||||
@ -572,6 +568,8 @@ class ArtifactExtractor
|
||||
|
||||
// Clean up temp directory
|
||||
FileSystem::removeDir($temp_dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -585,6 +583,7 @@ class ArtifactExtractor
|
||||
'{source_path}' => SOURCE_PATH,
|
||||
'{download_path}' => DOWNLOAD_PATH,
|
||||
'{working_dir}' => WORKING_DIR,
|
||||
'{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '',
|
||||
];
|
||||
return str_replace(array_keys($replacement), array_values($replacement), $path);
|
||||
}
|
||||
@ -627,9 +626,9 @@ class ArtifactExtractor
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFile(string $source_file, string $target_path): void
|
||||
private function copyFile(string $source_file, string $target_path): bool
|
||||
{
|
||||
FileSystem::createDir(dirname($target_path));
|
||||
FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
|
||||
return FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,8 @@ class DownloadResult
|
||||
) {
|
||||
switch ($this->cache_type) {
|
||||
case 'archive':
|
||||
$this->filename !== null ?: throw new DownloaderException('Archive download result must have a filename.');
|
||||
case 'file':
|
||||
$this->filename !== null ?: throw new DownloaderException('Archive/file download result must have a filename.');
|
||||
$fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename;
|
||||
file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}");
|
||||
break;
|
||||
@ -60,7 +61,20 @@ class DownloadResult
|
||||
?string $version = null,
|
||||
array $metadata = []
|
||||
): DownloadResult {
|
||||
return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
|
||||
// judge if it is archive or just a pure file
|
||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
|
||||
return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
|
||||
}
|
||||
|
||||
public static function file(
|
||||
string $filename,
|
||||
array $config,
|
||||
bool $verified = false,
|
||||
?string $version = null,
|
||||
array $metadata = []
|
||||
): DownloadResult {
|
||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
|
||||
return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -143,4 +157,16 @@ class DownloadResult
|
||||
array_merge($this->metadata, [$key => $value])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check
|
||||
*/
|
||||
private static function isArchiveFile(string $filename): bool
|
||||
{
|
||||
$archive_extensions = [
|
||||
'zip', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz', 'txz', 'rar', '7z',
|
||||
];
|
||||
$lower_filename = strtolower($filename);
|
||||
return array_any($archive_extensions, fn ($ext) => str_ends_with($lower_filename, '.' . $ext));
|
||||
}
|
||||
}
|
||||
|
||||
37
src/StaticPHP/Command/Dev/EnvCommand.php
Normal file
37
src/StaticPHP/Command/Dev/EnvCommand.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand('dev:env', 'Returns the internally defined environment variables')]
|
||||
class EnvCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('env', InputArgument::REQUIRED, 'The environment variable to show, if not set, all will be shown');
|
||||
}
|
||||
|
||||
public function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->no_motd = true;
|
||||
parent::initialize($input, $output);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$env = $this->getArgument('env');
|
||||
if (($val = getenv($env)) === false) {
|
||||
$this->output->writeln("<error>Environment variable '{$env}' is not set.</error>");
|
||||
return static::FAILURE;
|
||||
}
|
||||
$this->output->writeln("<info>{$val}</info>");
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ class DoctorCommand extends BaseCommand
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
|
||||
$fix_policy = match ($this->input->getOption('auto-fix')) {
|
||||
'never' => FIX_POLICY_DIE,
|
||||
true, null => FIX_POLICY_AUTOFIX,
|
||||
|
||||
@ -6,6 +6,7 @@ namespace StaticPHP;
|
||||
|
||||
use StaticPHP\Command\BuildLibsCommand;
|
||||
use StaticPHP\Command\BuildTargetCommand;
|
||||
use StaticPHP\Command\Dev\EnvCommand;
|
||||
use StaticPHP\Command\Dev\IsInstalledCommand;
|
||||
use StaticPHP\Command\Dev\ShellCommand;
|
||||
use StaticPHP\Command\DoctorCommand;
|
||||
@ -57,6 +58,7 @@ class ConsoleApplication extends Application
|
||||
// dev commands
|
||||
new ShellCommand(),
|
||||
new IsInstalledCommand(),
|
||||
new EnvCommand(),
|
||||
]);
|
||||
|
||||
// add additional commands from registries
|
||||
|
||||
@ -130,6 +130,7 @@ readonly class Doctor
|
||||
$this->output?->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
|
||||
$this->output?->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} finally {
|
||||
|
||||
142
src/StaticPHP/Doctor/Item/WindowsToolCheck.php
Normal file
142
src/StaticPHP/Doctor/Item/WindowsToolCheck.php
Normal file
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Toolchain\ToolchainManager;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
|
||||
#[OptionalCheck([self::class, 'optional'])]
|
||||
class WindowsToolCheck
|
||||
{
|
||||
public static function optional(): bool
|
||||
{
|
||||
return SystemTarget::getTargetOS() === 'Windows';
|
||||
}
|
||||
|
||||
#[CheckItem('if vswhere is installed', level: 999)]
|
||||
public function findVSWhere(): ?CheckResult
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('vswhere');
|
||||
$is_installed = $installer->isPackageInstalled('vswhere');
|
||||
if ($is_installed) {
|
||||
return CheckResult::ok();
|
||||
}
|
||||
return CheckResult::fail('vswhere is not installed', 'install-vswhere');
|
||||
}
|
||||
|
||||
#[CheckItem('if Visual Studio is installed', level: 998)]
|
||||
public function findVS(): ?CheckResult
|
||||
{
|
||||
$a = WindowsUtil::findVisualStudio();
|
||||
if ($a !== false) {
|
||||
return CheckResult::ok("{$a['version']} at {$a['dir']}");
|
||||
}
|
||||
return CheckResult::fail('Visual Studio with C++ tools is not installed. Please install Visual Studio with C++ tools.');
|
||||
}
|
||||
|
||||
#[CheckItem('if git associated command exists', level: 997)]
|
||||
public function checkGitPatch(): ?CheckResult
|
||||
{
|
||||
if (WindowsUtil::findCommand('patch.exe') === null) {
|
||||
return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.');
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)]
|
||||
public function checkSDK(): ?CheckResult
|
||||
{
|
||||
if (!file_exists(getenv('PHP_SDK_PATH') . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) {
|
||||
return CheckResult::fail('php-sdk-binary-tools not downloaded', 'install-php-sdk');
|
||||
}
|
||||
return CheckResult::ok(getenv('PHP_SDK_PATH'));
|
||||
}
|
||||
|
||||
#[CheckItem('if nasm installed', level: 995)]
|
||||
public function checkNasm(): ?CheckResult
|
||||
{
|
||||
if (($a = WindowsUtil::findCommand('nasm.exe')) === null) {
|
||||
return CheckResult::fail('nasm.exe not found in path.', 'install-nasm');
|
||||
}
|
||||
return CheckResult::ok($a);
|
||||
}
|
||||
|
||||
#[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
|
||||
public function checkPerl(): ?CheckResult
|
||||
{
|
||||
if (($path = WindowsUtil::findCommand('perl.exe')) === null) {
|
||||
return CheckResult::fail('perl not found in path.', 'install-perl');
|
||||
}
|
||||
if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) {
|
||||
return CheckResult::fail($path . ' is not built for msvc.', 'install-perl');
|
||||
}
|
||||
return CheckResult::ok($path);
|
||||
}
|
||||
|
||||
#[CheckItem('if environment is properly set up', level: 1)]
|
||||
public function checkenv(): ?CheckResult
|
||||
{
|
||||
// manually trigger after init
|
||||
try {
|
||||
ToolchainManager::afterInitToolchain();
|
||||
} catch (\Exception $e) {
|
||||
return CheckResult::fail('Environment setup failed: ' . $e->getMessage());
|
||||
}
|
||||
$required_cmd = ['cl.exe', 'link.exe', 'lib.exe', 'dumpbin.exe', 'msbuild.exe', 'nmake.exe'];
|
||||
foreach ($required_cmd as $cmd) {
|
||||
if (WindowsUtil::findCommand($cmd) === null) {
|
||||
return CheckResult::fail("{$cmd} not found in path. Please make sure Visual Studio with C++ tools is properly installed.");
|
||||
}
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[FixItem('install-perl')]
|
||||
public function installPerl(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('strawberry-perl');
|
||||
$installer->run(false);
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl');
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-php-sdk')]
|
||||
public function installSDK(): bool
|
||||
{
|
||||
FileSystem::removeDir(getenv('PHP_SDK_PATH'));
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('php-sdk-binary-tools');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-nasm')]
|
||||
public function installNasm(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('nasm');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-vswhere')]
|
||||
public function installVSWhere(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('vswhere');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,6 @@ use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
@ -27,9 +26,6 @@ class PackageBuilder
|
||||
{
|
||||
ApplicationContext::set(PackageBuilder::class, $this);
|
||||
|
||||
// apply build toolchain envs
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
$this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
@ -120,6 +121,9 @@ class PackageInstaller
|
||||
*/
|
||||
public function run(bool $interactive = true, bool $disable_delay_msg = false): void
|
||||
{
|
||||
// apply build toolchain envs
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
if (empty($this->packages)) {
|
||||
// resolve input, make dependency graph
|
||||
$this->resolvePackages();
|
||||
|
||||
@ -42,7 +42,7 @@ class DefaultShell extends Shell
|
||||
$cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}";
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
$result = $this->passthru($cmd, console_output: false, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
|
||||
$ret = $result['code'];
|
||||
$output = $result['output'];
|
||||
if ($ret !== 0) {
|
||||
@ -96,15 +96,15 @@ class DefaultShell extends Shell
|
||||
$cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[GIT CLONE] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
if ($submodules !== null) {
|
||||
$depth_flag = $shallow ? '--depth 1' : '';
|
||||
foreach ($submodules as $submodule) {
|
||||
$submodule = escapeshellarg($submodule);
|
||||
$submodule_cmd = clean_spaces("cd {$path_arg} && {$git} submodule update --init {$depth_flag} {$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, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($submodule_cmd, $this->console_putput, cwd: $path_arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,7 +117,7 @@ class DefaultShell extends Shell
|
||||
* @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): void
|
||||
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));
|
||||
@ -135,7 +135,8 @@ class DefaultShell extends Shell
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[TAR EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,7 +155,7 @@ class DefaultShell extends Shell
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[UNZIP] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,9 +163,8 @@ class DefaultShell extends Shell
|
||||
*
|
||||
* @param string $archive_path Path to the archive file
|
||||
* @param string $target_path Path to extract to
|
||||
* @param bool $is_txz Whether this is a .txz/.tar.xz file that needs double extraction
|
||||
*/
|
||||
public function execute7zExtract(string $archive_path, string $target_path, bool $is_txz = false): void
|
||||
public function execute7zExtract(string $archive_path, string $target_path): bool
|
||||
{
|
||||
$sdk_path = getenv('PHP_SDK_PATH');
|
||||
if ($sdk_path === false) {
|
||||
@ -177,15 +177,19 @@ class DefaultShell extends Shell
|
||||
|
||||
$mute = $this->console_putput ? '' : ' > NUL';
|
||||
|
||||
if ($is_txz) {
|
||||
// txz/tar.xz contains a tar file inside, extract twice
|
||||
$cmd = "{$_7z} x {$archive_arg} -so | {$_7z} x -si -ttar -o{$target_arg} -y{$mute}";
|
||||
} else {
|
||||
$cmd = "{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}";
|
||||
}
|
||||
$run = function ($cmd) {
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[7Z EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
};
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[7Z EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$extname = FileSystem::extname($archive_path);
|
||||
match ($extname) {
|
||||
'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'),
|
||||
'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar tar -f - -x -C {$target_arg} --strip-components 1"),
|
||||
default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,8 +148,12 @@ abstract class Shell
|
||||
bool $console_output = false,
|
||||
?string $original_command = null,
|
||||
bool $capture_output = false,
|
||||
bool $throw_on_error = true
|
||||
bool $throw_on_error = true,
|
||||
?string $cwd = null
|
||||
): array {
|
||||
if ($cwd !== null) {
|
||||
$cwd = $cwd;
|
||||
}
|
||||
$file_res = null;
|
||||
if ($this->enable_log_file) {
|
||||
// write executed command to the log file using fwrite
|
||||
@ -160,10 +164,10 @@ abstract class Shell
|
||||
}
|
||||
$descriptors = [
|
||||
0 => ['file', 'php://stdin', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'], // stderr
|
||||
1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout
|
||||
2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr
|
||||
];
|
||||
$process = proc_open($cmd, $descriptors, $pipes);
|
||||
$process = proc_open($cmd, $descriptors, $pipes, $cwd);
|
||||
|
||||
$output_value = '';
|
||||
try {
|
||||
|
||||
@ -33,7 +33,7 @@ class UnixShell extends Shell
|
||||
$original_command = $cmd;
|
||||
$this->logCommandInfo($original_command);
|
||||
$this->last_cmd = $cmd = $this->getExecString($cmd);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ class UnixShell extends Shell
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
$this->logCommandInfo($cmd);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
@ -83,9 +83,6 @@ class UnixShell extends Shell
|
||||
if (!empty($env_str)) {
|
||||
$cmd = "{$env_str} {$cmd}";
|
||||
}
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\Exception\ExecutionException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
@ -28,7 +27,7 @@ class WindowsCmd extends Shell
|
||||
$this->last_cmd = $cmd = $this->getExecString($cmd);
|
||||
// echo $cmd . PHP_EOL;
|
||||
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -46,7 +45,7 @@ class WindowsCmd extends Shell
|
||||
logger()->debug('Running command with result: ' . $cmd);
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
@ -68,89 +67,8 @@ class WindowsCmd extends Shell
|
||||
return $this->last_cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command with console and log file output.
|
||||
*
|
||||
* @param string $cmd Full command to execute (including cd and env vars)
|
||||
* @param bool $console_output If true, output will be printed to console
|
||||
* @param null|string $original_command Original command string for logging
|
||||
* @param bool $capture_output If true, capture and return output
|
||||
* @param bool $throw_on_error If true, throw exception on non-zero exit code
|
||||
*
|
||||
* @return array{code: int, output: string} Returns exit code and captured output
|
||||
*/
|
||||
protected function passthru(
|
||||
string $cmd,
|
||||
bool $console_output = false,
|
||||
?string $original_command = null,
|
||||
bool $capture_output = false,
|
||||
bool $throw_on_error = true
|
||||
): array {
|
||||
$file_res = null;
|
||||
if ($this->enable_log_file) {
|
||||
$file_res = fopen(SPC_SHELL_LOG, 'a');
|
||||
}
|
||||
|
||||
$output_value = '';
|
||||
try {
|
||||
$process = popen($cmd . ' 2>&1', 'r');
|
||||
if (!$process) {
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: 'Failed to open process for command, popen() failed.',
|
||||
code: -1,
|
||||
cd: $this->cd,
|
||||
env: $this->env
|
||||
);
|
||||
}
|
||||
|
||||
while (($line = fgets($process)) !== false) {
|
||||
if (static::$passthru_callback !== null) {
|
||||
$callback = static::$passthru_callback;
|
||||
$callback();
|
||||
}
|
||||
if ($console_output) {
|
||||
echo $line;
|
||||
}
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, $line);
|
||||
}
|
||||
if ($capture_output) {
|
||||
$output_value .= $line;
|
||||
}
|
||||
}
|
||||
|
||||
$result_code = pclose($process);
|
||||
|
||||
if ($throw_on_error && $result_code !== 0) {
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, "Command exited with non-zero code: {$result_code}\n");
|
||||
}
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: "Command exited with non-zero code: {$result_code}",
|
||||
code: $result_code,
|
||||
cd: $this->cd,
|
||||
env: $this->env,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $result_code,
|
||||
'output' => $output_value,
|
||||
];
|
||||
} finally {
|
||||
if ($file_res !== null) {
|
||||
fclose($file_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getExecString(string $cmd): string
|
||||
{
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,57 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Toolchain;
|
||||
|
||||
use StaticPHP\Exception\EnvironmentException;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
|
||||
class MSVCToolchain implements ToolchainInterface
|
||||
{
|
||||
public function initEnv(): void {}
|
||||
public function initEnv(): void
|
||||
{
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\bin');
|
||||
$sdk = getenv('PHP_SDK_PATH');
|
||||
if ($sdk !== false) {
|
||||
GlobalEnvManager::addPathIfNotExists($sdk . '\bin');
|
||||
GlobalEnvManager::addPathIfNotExists($sdk . '\msys2\usr\bin');
|
||||
}
|
||||
// strawberry-perl
|
||||
if (is_dir(PKG_ROOT_PATH . '\strawberry-perl')) {
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl\perl\bin');
|
||||
}
|
||||
}
|
||||
|
||||
public function afterInit(): void {}
|
||||
public function afterInit(): void
|
||||
{
|
||||
$count = count(getenv());
|
||||
$vs = WindowsUtil::findVisualStudio();
|
||||
if ($vs === false || !file_exists($vcvarsall = "{$vs['dir']}\\VC\\Auxiliary\\Build\\vcvarsall.bat")) {
|
||||
throw new EnvironmentException(
|
||||
'Visual Studio with C++ tools not found',
|
||||
'Please install Visual Studio with C++ tools'
|
||||
);
|
||||
}
|
||||
if (getenv('VCINSTALLDIR') === false) {
|
||||
if (file_exists(DOWNLOAD_PATH . '/.vcenv-cache') && (time() - filemtime(DOWNLOAD_PATH . '/.vcenv-cache')) < 3600) {
|
||||
$output = file(DOWNLOAD_PATH . '/.vcenv-cache', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
} else {
|
||||
exec('call "' . $vcvarsall . '" x64 > NUL && set', $output);
|
||||
file_put_contents(DOWNLOAD_PATH . '/.vcenv-cache', implode("\n", $output));
|
||||
}
|
||||
array_map(fn ($x) => putenv($x), $output);
|
||||
}
|
||||
$after = count(getenv());
|
||||
if ($after > $count) {
|
||||
logger()->debug('Applied ' . ($after - $count) . ' environment variables from Visual Studio setup');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCompilerInfo(): ?string
|
||||
{
|
||||
if ($vcver = getenv('VisualStudioVersion')) {
|
||||
return "Visual Studio {$vcver}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ class FileSystem
|
||||
$src_path = FileSystem::convertPath($from);
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
|
||||
cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
|
||||
break;
|
||||
case 'Linux':
|
||||
case 'Darwin':
|
||||
@ -137,7 +137,7 @@ class FileSystem
|
||||
* @param string $from Source file path
|
||||
* @param string $to Destination file path
|
||||
*/
|
||||
public static function copy(string $from, string $to): void
|
||||
public static function copy(string $from, string $to): bool
|
||||
{
|
||||
logger()->debug("Copying file from {$from} to {$to}");
|
||||
$dst_path = FileSystem::convertPath($to);
|
||||
@ -145,6 +145,7 @@ class FileSystem
|
||||
if (!copy($src_path, $dst_path)) {
|
||||
throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -317,7 +318,12 @@ class FileSystem
|
||||
}
|
||||
} elseif (is_link($sub_file) || is_file($sub_file)) {
|
||||
if (!unlink($sub_file)) {
|
||||
return false;
|
||||
$cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f';
|
||||
f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret);
|
||||
if ($ret !== 0) {
|
||||
logger()->warning('Remove file failed: ' . $sub_file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +107,8 @@ class GlobalEnvManager
|
||||
{
|
||||
if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) {
|
||||
self::putenv("PATH={$path}:" . getenv('PATH'));
|
||||
} elseif (SystemTarget::getTargetOS() === 'Windows' && !str_contains(getenv('PATH'), $path)) {
|
||||
self::putenv("PATH={$path};" . getenv('PATH'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,13 +15,10 @@ class WindowsUtil
|
||||
* @param array $paths search path (default use env path)
|
||||
* @return null|string null if not found, string is absolute path
|
||||
*/
|
||||
public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string
|
||||
public static function findCommand(string $name, array $paths = []): ?string
|
||||
{
|
||||
if (!$paths) {
|
||||
$paths = explode(PATH_SEPARATOR, getenv('Path'));
|
||||
if ($include_sdk_bin) {
|
||||
$paths[] = getenv('PHP_SDK_PATH') . '\bin';
|
||||
}
|
||||
}
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
|
||||
@ -34,29 +31,35 @@ class WindowsUtil
|
||||
/**
|
||||
* Find Visual Studio installation.
|
||||
*
|
||||
* @return array<string, string>|false False if not installed, array contains 'version' and 'dir'
|
||||
* @return array{
|
||||
* version: string,
|
||||
* major_version: string,
|
||||
* dir: string
|
||||
* }|false False if not installed, array contains 'version' and 'dir'
|
||||
*/
|
||||
public static function findVisualStudio(): array|false
|
||||
{
|
||||
$check_path = [
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
// call vswhere (need VS and C++ tools installed), output is json
|
||||
$vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe';
|
||||
$args = [
|
||||
'-latest',
|
||||
'-format', 'json',
|
||||
'-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
|
||||
];
|
||||
foreach ($check_path as $path => $vs_version) {
|
||||
if (file_exists($path)) {
|
||||
$vs_ver = $vs_version;
|
||||
$d_dir = dirname($path, 4);
|
||||
return [
|
||||
'version' => $vs_ver,
|
||||
'dir' => $d_dir,
|
||||
];
|
||||
}
|
||||
$cmd = escapeshellarg($vswhere_exec) . ' ' . implode(' ', $args);
|
||||
$result = f_exec($cmd, $out, $code);
|
||||
if ($code !== 0 || !$result) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
$json = json_decode(implode("\n", $out), true);
|
||||
if (!is_array($json) || count($json) === 0) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'version' => $json[0]['installationVersion'],
|
||||
'major_version' => explode('.', $json[0]['installationVersion'])[0],
|
||||
'dir' => $json[0]['installationPath'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user