From 2080407283be1a314174065571b58a505ce81386 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 11 Dec 2025 11:35:12 +0800 Subject: [PATCH] Enhance Windows support by updating artifact configuration and improving extraction logic --- captainhook.json | 2 +- config/artifact.json | 6 +- config/pkg.target.json | 5 +- src/StaticPHP/Artifact/Artifact.php | 19 ++- src/StaticPHP/Artifact/ArtifactExtractor.php | 59 ++++---- .../Artifact/Downloader/DownloadResult.php | 30 +++- src/StaticPHP/Command/Dev/EnvCommand.php | 37 +++++ src/StaticPHP/Command/DoctorCommand.php | 1 + src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Doctor/Doctor.php | 1 + .../Doctor/Item/WindowsToolCheck.php | 142 ++++++++++++++++++ src/StaticPHP/Package/PackageBuilder.php | 4 - src/StaticPHP/Package/PackageInstaller.php | 4 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 40 ++--- src/StaticPHP/Runtime/Shell/Shell.php | 12 +- src/StaticPHP/Runtime/Shell/UnixShell.php | 7 +- src/StaticPHP/Runtime/Shell/WindowsCmd.php | 86 +---------- src/StaticPHP/Toolchain/MSVCToolchain.php | 45 +++++- src/StaticPHP/Util/FileSystem.php | 12 +- src/StaticPHP/Util/GlobalEnvManager.php | 2 + src/StaticPHP/Util/System/WindowsUtil.php | 47 +++--- 21 files changed, 382 insertions(+), 181 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/EnvCommand.php create mode 100644 src/StaticPHP/Doctor/Item/WindowsToolCheck.php diff --git a/captainhook.json b/captainhook.json index c4c1d8b9..63d88f06 100644 --- a/captainhook.json +++ b/captainhook.json @@ -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", diff --git a/config/artifact.json b/config/artifact.json index de30a1e4..75ee9cfc 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -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": { diff --git a/config/pkg.target.json b/config/pkg.target.json index b5e4a7c8..2ae49f40 100644 --- a/config/pkg.target.json +++ b/config/pkg.target.json @@ -1,7 +1,10 @@ { "vswhere": { "type": "target", - "artifact": "vswhere" + "artifact": "vswhere", + "static-bins@windows": [ + "vswhere.exe" + ] }, "pkg-config": { "type": "target", diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 5e5e8b55..bcf6ca62 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -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']; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 778c24f3..93b36382 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -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); } } diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index aefc6716..6fa40bed 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -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)); + } } diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php new file mode 100644 index 00000000..2f2dbcf0 --- /dev/null +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -0,0 +1,37 @@ +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("Environment variable '{$env}' is not set."); + return static::FAILURE; + } + $this->output->writeln("{$val}"); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 30475a5e..cd90cd94 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -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, diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index a12227fc..0e5371ac 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -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 diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 22ca10f2..d86e42ac 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -130,6 +130,7 @@ readonly class Doctor $this->output?->writeln('Fix failed: ' . $e->getMessage() . ''); return false; } catch (\Throwable $e) { + logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString()); $this->output?->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); return false; } finally { diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php new file mode 100644 index 00000000..e6a042d3 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -0,0 +1,142 @@ +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; + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index c87b7f29..e2373253 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -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; } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 53000397..4c44ce92 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -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(); diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index ee6f790c..88393289 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -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; } } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index e465f66e..1368c017 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -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 { diff --git a/src/StaticPHP/Runtime/Shell/UnixShell.php b/src/StaticPHP/Runtime/Shell/UnixShell.php index 7690f247..7d74f65f 100644 --- a/src/StaticPHP/Runtime/Shell/UnixShell.php +++ b/src/StaticPHP/Runtime/Shell/UnixShell.php @@ -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; } } diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index 5a7511a9..a60c41b2 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -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; } } diff --git a/src/StaticPHP/Toolchain/MSVCToolchain.php b/src/StaticPHP/Toolchain/MSVCToolchain.php index 68a2d0a2..1449db70 100644 --- a/src/StaticPHP/Toolchain/MSVCToolchain.php +++ b/src/StaticPHP/Toolchain/MSVCToolchain.php @@ -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; } diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 46d15a1e..8eb98e40 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -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; + } } } } diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index cc21b480..86fcc652 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -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')); } } diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index 1d911161..a6df4156 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -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|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'], + ]; } /**