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