Enhance Windows support by updating artifact configuration and improving extraction logic

This commit is contained in:
crazywhalecc 2025-12-11 11:35:12 +08:00
parent dc05ad23c9
commit 2080407283
21 changed files with 382 additions and 181 deletions

View File

@ -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",

View File

@ -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": {

View File

@ -1,7 +1,10 @@
{
"vswhere": {
"type": "target",
"artifact": "vswhere"
"artifact": "vswhere",
"static-bins@windows": [
"vswhere.exe"
]
},
"pkg-config": {
"type": "target",

View File

@ -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'];

View File

@ -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);
}
}

View File

@ -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));
}
}

View 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;
}
}

View File

@ -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,

View File

@ -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

View File

@ -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 {

View 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;
}
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}

View File

@ -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'));
}
}

View File

@ -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'],
];
}
/**