mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-10 10:25:36 +08:00
v3 base
This commit is contained in:
17
src/StaticPHP/Runtime/Executor/Executor.php
Normal file
17
src/StaticPHP/Runtime/Executor/Executor.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Executor;
|
||||
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
|
||||
abstract class Executor
|
||||
{
|
||||
public function __construct(protected LibraryPackage $package) {}
|
||||
|
||||
public static function create(LibraryPackage $package): static
|
||||
{
|
||||
return new static($package);
|
||||
}
|
||||
}
|
||||
198
src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php
Normal file
198
src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Executor;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\Shell\UnixShell;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
class UnixAutoconfExecutor extends Executor
|
||||
{
|
||||
protected UnixShell $shell;
|
||||
|
||||
protected array $configure_args = [];
|
||||
|
||||
protected array $ignore_args = [];
|
||||
|
||||
protected PackageInstaller $installer;
|
||||
|
||||
public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null)
|
||||
{
|
||||
parent::__construct($package);
|
||||
if ($installer !== null) {
|
||||
$this->installer = $installer;
|
||||
} elseif (ApplicationContext::has(PackageInstaller::class)) {
|
||||
$this->installer = ApplicationContext::get(PackageInstaller::class);
|
||||
} else {
|
||||
throw new SPCInternalException('PackageInstaller not found in container');
|
||||
}
|
||||
$this->initShell();
|
||||
|
||||
// judge that this package has artifact.source and defined build stage
|
||||
if (!$this->package->hasStage('build')) {
|
||||
throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run ./configure
|
||||
*/
|
||||
public function configure(...$args): static
|
||||
{
|
||||
// remove all the ignored args
|
||||
$args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args);
|
||||
$args = array_diff($args, $this->ignore_args);
|
||||
$configure_args = implode(' ', $args);
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)');
|
||||
return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}"));
|
||||
}
|
||||
|
||||
public function getConfigureArgsString(): string
|
||||
{
|
||||
return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run make
|
||||
*
|
||||
* @param string $target Build target
|
||||
* @param false|string $with_install Run `make install` after building, or false to skip
|
||||
* @param bool $with_clean Whether to clean before building
|
||||
* @param array $after_env_vars Environment variables postfix
|
||||
*/
|
||||
public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static
|
||||
{
|
||||
return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) {
|
||||
$shell = $this->shell;
|
||||
if ($dir) {
|
||||
$shell = $shell->cd($dir);
|
||||
}
|
||||
if ($with_clean) {
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make clean)');
|
||||
$shell->exec('make clean');
|
||||
}
|
||||
$after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : '';
|
||||
$concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency;
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make)');
|
||||
$shell->exec("make -j{$concurrency} {$target} {$after_env_vars_str}");
|
||||
if ($with_install !== false) {
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make ' . $with_install . ')');
|
||||
$shell->exec("make {$with_install}");
|
||||
}
|
||||
return $shell;
|
||||
});
|
||||
}
|
||||
|
||||
public function exec(string $cmd): static
|
||||
{
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()));
|
||||
$this->shell->exec($cmd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add optional library configuration.
|
||||
* This method checks if a library is available and adds the corresponding arguments to the CMake configuration.
|
||||
*
|
||||
* @param string $name library name to check
|
||||
* @param \Closure|string $true_args arguments to use if the library is available (allow closure, returns string)
|
||||
* @param string $false_args arguments to use if the library is not available
|
||||
* @return $this
|
||||
*/
|
||||
public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static
|
||||
{
|
||||
if ($get = $this->installer->getResolvedPackages()[$name] ?? null) {
|
||||
logger()->info("Building package [{$this->package->getName()}] with {$name} support");
|
||||
$args = $true_args instanceof \Closure ? $true_args($get) : $true_args;
|
||||
} else {
|
||||
logger()->info("Building package [{$this->package->getName()}] without {$name} support");
|
||||
$args = $false_args;
|
||||
}
|
||||
$this->addConfigureArgs($args);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add configure args.
|
||||
*/
|
||||
public function addConfigureArgs(...$args): static
|
||||
{
|
||||
$this->configure_args = [...$this->configure_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some configure args, to bypass the configure option checking for some libs.
|
||||
*/
|
||||
public function removeConfigureArgs(...$args): static
|
||||
{
|
||||
$this->ignore_args = [...$this->ignore_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
$this->shell->setEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
$this->shell->appendEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default autoconf ./configure arguments
|
||||
*/
|
||||
private function getDefaultConfigureArgs(): array
|
||||
{
|
||||
return [
|
||||
'--disable-shared',
|
||||
'--enable-static',
|
||||
"--prefix={$this->package->getBuildRootPath()}",
|
||||
'--with-pic',
|
||||
'--enable-pic',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize UnixShell class.
|
||||
*/
|
||||
private function initShell(): void
|
||||
{
|
||||
$this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([
|
||||
'CFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'CXXFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'LDFLAGS' => "-L{$this->package->getLibDir()}",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When an exception occurs, this method will check if the config log file exists.
|
||||
*/
|
||||
private function seekLogFileOnException(mixed $callable): static
|
||||
{
|
||||
try {
|
||||
$callable();
|
||||
return $this;
|
||||
} catch (SPCException $e) {
|
||||
if (file_exists("{$this->package->getSourceDir()}/config.log")) {
|
||||
logger()->debug("Config log file found: {$this->package->getSourceDir()}/config.log");
|
||||
$log_file = "lib.{$this->package->getName()}.console.log";
|
||||
logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}");
|
||||
$e->addExtraLogFile("{$this->package->getName()} library config.log", $log_file);
|
||||
copy("{$this->package->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}");
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
333
src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php
Normal file
333
src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php
Normal file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Executor;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\Shell\UnixShell;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\PkgConfigUtil;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
/**
|
||||
* Unix-like OS cmake command executor.
|
||||
*/
|
||||
class UnixCMakeExecutor extends Executor
|
||||
{
|
||||
protected UnixShell $shell;
|
||||
|
||||
protected array $configure_args = [];
|
||||
|
||||
protected array $ignore_args = [];
|
||||
|
||||
protected ?string $build_dir = null;
|
||||
|
||||
protected ?array $custom_default_args = null;
|
||||
|
||||
protected int $steps = 3;
|
||||
|
||||
protected bool $reset = true;
|
||||
|
||||
protected PackageInstaller $installer;
|
||||
|
||||
public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null)
|
||||
{
|
||||
parent::__construct($package);
|
||||
if ($installer !== null) {
|
||||
$this->installer = $installer;
|
||||
} elseif (ApplicationContext::has(PackageInstaller::class)) {
|
||||
$this->installer = ApplicationContext::get(PackageInstaller::class);
|
||||
} else {
|
||||
throw new SPCInternalException('PackageInstaller not found in container');
|
||||
}
|
||||
$this->initShell();
|
||||
|
||||
// judge that this package has artifact.source and defined build stage
|
||||
if (!$this->package->hasStage('build')) {
|
||||
throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run cmake configure, build and install.
|
||||
*
|
||||
* @param string $build_pos Build position relative to build directory
|
||||
*/
|
||||
public function build(string $build_pos = '..'): static
|
||||
{
|
||||
return $this->seekLogFileOnException(function () use ($build_pos) {
|
||||
// set cmake dir
|
||||
$this->initBuildDir();
|
||||
|
||||
if ($this->reset) {
|
||||
FileSystem::resetDir($this->build_dir);
|
||||
}
|
||||
|
||||
$this->shell = $this->shell->cd($this->build_dir);
|
||||
|
||||
// config
|
||||
if ($this->steps >= 1) {
|
||||
$args = array_merge($this->configure_args, $this->getDefaultCMakeArgs());
|
||||
$args = array_diff($args, $this->ignore_args);
|
||||
$configure_args = implode(' ', $args);
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake configure)');
|
||||
$this->shell->exec("cmake {$configure_args} {$build_pos}");
|
||||
}
|
||||
|
||||
// make
|
||||
if ($this->steps >= 2) {
|
||||
$concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency;
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake build)');
|
||||
$this->shell->exec("cmake --build . -j {$concurrency}");
|
||||
}
|
||||
|
||||
// install
|
||||
if ($this->steps >= 3) {
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake install)');
|
||||
$this->shell->exec('make install');
|
||||
}
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a custom command.
|
||||
*/
|
||||
public function exec(string $cmd): static
|
||||
{
|
||||
$this->shell->exec($cmd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add optional package configuration.
|
||||
* This method checks if a package is available and adds the corresponding arguments to the CMake configuration.
|
||||
*
|
||||
* @param string $name package name to check
|
||||
* @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string)
|
||||
* @param string $false_args arguments to use if the package is not available
|
||||
* @return $this
|
||||
*/
|
||||
public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static
|
||||
{
|
||||
if ($get = $this->installer->getResolvedPackages()[$name] ?? null) {
|
||||
logger()->info("Building package [{$this->package->getName()}] with {$name} support");
|
||||
$args = $true_args instanceof \Closure ? $true_args($get) : $true_args;
|
||||
} else {
|
||||
logger()->info("Building package [{$this->package->getName()}] without {$name} support");
|
||||
$args = $false_args;
|
||||
}
|
||||
$this->addConfigureArgs($args);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add configure args.
|
||||
*/
|
||||
public function addConfigureArgs(...$args): static
|
||||
{
|
||||
$this->configure_args = [...$this->configure_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some configure args, to bypass the configure option checking for some libs.
|
||||
*/
|
||||
public function removeConfigureArgs(...$args): static
|
||||
{
|
||||
$this->ignore_args = [...$this->ignore_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
$this->shell->setEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
$this->shell->appendEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* To build steps.
|
||||
*
|
||||
* @param int $step Step number, accept 1-3
|
||||
* @return $this
|
||||
*/
|
||||
public function toStep(int $step): static
|
||||
{
|
||||
$this->steps = $step;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom CMake build directory.
|
||||
*
|
||||
* @param string $dir custom CMake build directory
|
||||
*/
|
||||
public function setBuildDir(string $dir): static
|
||||
{
|
||||
$this->build_dir = $dir;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom default args.
|
||||
*/
|
||||
public function setCustomDefaultArgs(...$args): static
|
||||
{
|
||||
$this->custom_default_args = $args;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reset status.
|
||||
* If we set it to false, it will not clean and create the specified cmake working directory.
|
||||
*/
|
||||
public function setReset(bool $reset): static
|
||||
{
|
||||
$this->reset = $reset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configure argument string.
|
||||
*/
|
||||
public function getConfigureArgsString(): string
|
||||
{
|
||||
return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default CMake args.
|
||||
*/
|
||||
private function getDefaultCMakeArgs(): array
|
||||
{
|
||||
return $this->custom_default_args ?? [
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
"-DCMAKE_INSTALL_PREFIX={$this->package->getBuildRootPath()}",
|
||||
'-DCMAKE_INSTALL_BINDIR=bin',
|
||||
'-DCMAKE_INSTALL_LIBDIR=lib',
|
||||
'-DCMAKE_INSTALL_INCLUDEDIR=include',
|
||||
'-DPOSITION_INDEPENDENT_CODE=ON',
|
||||
'-DBUILD_SHARED_LIBS=OFF',
|
||||
"-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the CMake build directory.
|
||||
* If the directory is not set, it defaults to the package's source directory with '/build' appended.
|
||||
*/
|
||||
private function initBuildDir(): void
|
||||
{
|
||||
if ($this->build_dir === null) {
|
||||
$this->build_dir = "{$this->package->getSourceDir()}/build";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cmake toolchain file for current spc instance, and return the file path.
|
||||
*
|
||||
* @return string CMake toolchain file path
|
||||
*/
|
||||
private function makeCmakeToolchainFile(): string
|
||||
{
|
||||
static $created;
|
||||
if (isset($created)) {
|
||||
return $created;
|
||||
}
|
||||
$os = PHP_OS_FAMILY;
|
||||
$target_arch = arch2gnu(php_uname('m'));
|
||||
$cflags = getenv('SPC_DEFAULT_C_FLAGS');
|
||||
$cc = getenv('CC');
|
||||
$cxx = getenv('CXX');
|
||||
logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'");
|
||||
$root = BUILD_ROOT_PATH;
|
||||
$pkgConfigExecutable = PkgConfigUtil::findPkgConfig();
|
||||
$ccLine = '';
|
||||
if ($cc) {
|
||||
$ccLine = 'SET(CMAKE_C_COMPILER ' . $cc . ')';
|
||||
}
|
||||
$cxxLine = '';
|
||||
if ($cxx) {
|
||||
$cxxLine = 'SET(CMAKE_CXX_COMPILER ' . $cxx . ')';
|
||||
}
|
||||
$toolchain = <<<CMAKE
|
||||
{$ccLine}
|
||||
{$cxxLine}
|
||||
SET(CMAKE_C_FLAGS "{$cflags}")
|
||||
SET(CMAKE_CXX_FLAGS "{$cflags}")
|
||||
SET(CMAKE_FIND_ROOT_PATH "{$root}")
|
||||
SET(CMAKE_PREFIX_PATH "{$root}")
|
||||
SET(CMAKE_INSTALL_PREFIX "{$root}")
|
||||
SET(CMAKE_INSTALL_LIBDIR "lib")
|
||||
|
||||
set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}")
|
||||
set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "-ldl -lpthread -lm -lutil")
|
||||
CMAKE;
|
||||
// Whoops, linux may need CMAKE_AR sometimes
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
$toolchain .= "\nSET(CMAKE_AR \"ar\")";
|
||||
}
|
||||
FileSystem::writeFile(SOURCE_PATH . '/toolchain.cmake', $toolchain);
|
||||
return $created = realpath(SOURCE_PATH . '/toolchain.cmake');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize UnixShell class.
|
||||
*/
|
||||
private function initShell(): void
|
||||
{
|
||||
$this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([
|
||||
'CFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'CXXFLAGS' => "-I{$this->package->getIncludeDir()}",
|
||||
'LDFLAGS' => "-L{$this->package->getLibDir()}",
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When an exception occurs, this method will check if the cmake log file exists.
|
||||
*/
|
||||
private function seekLogFileOnException(mixed $callable): static
|
||||
{
|
||||
try {
|
||||
$callable();
|
||||
return $this;
|
||||
} catch (SPCException $e) {
|
||||
$cmake_log = "{$this->build_dir}/CMakeFiles/CMakeError.log";
|
||||
if (file_exists($cmake_log)) {
|
||||
logger()->debug("CMake error log file found: {$cmake_log}");
|
||||
$log_file = "lib.{$this->package->getName()}.cmake-error.log";
|
||||
logger()->debug('Saved CMake error log file to: ' . SPC_LOGS_DIR . "/{$log_file}");
|
||||
$e->addExtraLogFile("{$this->package->getName()} library CMakeError.log", $log_file);
|
||||
copy($cmake_log, SPC_LOGS_DIR . "/{$log_file}");
|
||||
}
|
||||
$cmake_output = "{$this->build_dir}/CMakeFiles/CMakeOutput.log";
|
||||
if (file_exists($cmake_output)) {
|
||||
logger()->debug("CMake output log file found: {$cmake_output}");
|
||||
$log_file = "lib.{$this->package->getName()}.cmake-output.log";
|
||||
logger()->debug('Saved CMake output log file to: ' . SPC_LOGS_DIR . "/{$log_file}");
|
||||
$e->addExtraLogFile("{$this->package->getName()} library CMakeOutput.log", $log_file);
|
||||
copy($cmake_output, SPC_LOGS_DIR . "/{$log_file}");
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
191
src/StaticPHP/Runtime/Shell/DefaultShell.php
Normal file
191
src/StaticPHP/Runtime/Shell/DefaultShell.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\Exception\InterruptException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
/**
|
||||
* A default shell implementation that does not support custom commands.
|
||||
* Used as a internal command caller when some place needs os-irrelevant shell.
|
||||
*/
|
||||
class DefaultShell extends Shell
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function exec(string $cmd): static
|
||||
{
|
||||
throw new SPCInternalException('DefaultShell does not support custom command execution.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a cURL command to fetch data from a URL.
|
||||
*/
|
||||
public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook($method, $url, $headers);
|
||||
}
|
||||
$url_arg = escapeshellarg($url);
|
||||
|
||||
$method_arg = match ($method) {
|
||||
'GET' => '',
|
||||
'HEAD' => '-I',
|
||||
default => "-X {$method}",
|
||||
};
|
||||
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
|
||||
$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);
|
||||
$ret = $result['code'];
|
||||
$output = $result['output'];
|
||||
if ($ret !== 0) {
|
||||
logger()->debug("[CURL ERROR] Command exited with code: {$ret}");
|
||||
}
|
||||
if ($ret === 2 || $ret === -1073741510) {
|
||||
throw new InterruptException(sprintf('Canceled fetching "%s"', $url));
|
||||
}
|
||||
if ($ret !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return trim($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a cURL command to download a file from a URL.
|
||||
*/
|
||||
public function executeCurlDownload(string $url, string $path, array $headers = [], array $hooks = [], int $retries = 0): void
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$hook('GET', $url, $headers);
|
||||
}
|
||||
$url_arg = escapeshellarg($url);
|
||||
$path_arg = escapeshellarg($path);
|
||||
|
||||
$header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers));
|
||||
$retry_arg = $retries > 0 ? "--retry {$retries}" : '';
|
||||
$check = $this->console_putput ? '#' : 's';
|
||||
$cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}");
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug('[CURL DOWNLOAD] ' . $cmd);
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Git clone command to clone a repository.
|
||||
*/
|
||||
public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void
|
||||
{
|
||||
$path = FileSystem::convertPath($path);
|
||||
if (file_exists($path)) {
|
||||
FileSystem::removeDir($path);
|
||||
}
|
||||
$git = SPC_GIT_EXEC;
|
||||
$url_arg = escapeshellarg($url);
|
||||
$branch_arg = escapeshellarg($branch);
|
||||
$path_arg = escapeshellarg($path);
|
||||
$shallow_arg = $shallow ? '--depth 1 --single-branch' : '';
|
||||
$submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : '');
|
||||
$cmd = clean_spaces("{$git} clone --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);
|
||||
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}");
|
||||
$this->logCommandInfo($submodule_cmd);
|
||||
logger()->debug("[GIT SUBMODULE] {$submodule_cmd}");
|
||||
$this->passthru($submodule_cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tar command to extract an archive.
|
||||
*
|
||||
* @param string $archive_path Path to the archive file
|
||||
* @param string $target_path Path to extract to
|
||||
* @param string $compression Compression type: 'gz', 'bz2', 'xz', or 'none'
|
||||
* @param int $strip Number of leading components to strip (default: 1)
|
||||
*/
|
||||
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): void
|
||||
{
|
||||
$archive_arg = escapeshellarg(FileSystem::convertPath($archive_path));
|
||||
$target_arg = escapeshellarg(FileSystem::convertPath($target_path));
|
||||
|
||||
$compression_flag = match ($compression) {
|
||||
'gz' => '-z',
|
||||
'bz2' => '-j',
|
||||
'xz' => '-J',
|
||||
'none' => '',
|
||||
default => throw new SPCInternalException("Unknown compression type: {$compression}"),
|
||||
};
|
||||
|
||||
$mute = $this->console_putput ? '' : ' 2>/dev/null';
|
||||
$cmd = "tar {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}";
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[TAR EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an unzip command to extract a zip archive.
|
||||
*
|
||||
* @param string $zip_path Path to the zip file
|
||||
* @param string $target_path Path to extract to
|
||||
*/
|
||||
public function executeUnzip(string $zip_path, string $target_path): void
|
||||
{
|
||||
$zip_arg = escapeshellarg(FileSystem::convertPath($zip_path));
|
||||
$target_arg = escapeshellarg(FileSystem::convertPath($target_path));
|
||||
|
||||
$mute = $this->console_putput ? '' : ' > /dev/null';
|
||||
$cmd = "unzip {$zip_arg} -d {$target_arg}{$mute}";
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[UNZIP] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a 7za command to extract an archive (Windows).
|
||||
*
|
||||
* @param string $archive_path Path to the archive file
|
||||
* @param string $target_path Path to extract to
|
||||
* @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
|
||||
{
|
||||
$sdk_path = getenv('PHP_SDK_PATH');
|
||||
if ($sdk_path === false) {
|
||||
throw new SPCInternalException('PHP_SDK_PATH environment variable is not set');
|
||||
}
|
||||
|
||||
$_7z = escapeshellarg(FileSystem::convertPath($sdk_path . '/bin/7za.exe'));
|
||||
$archive_arg = escapeshellarg(FileSystem::convertPath($archive_path));
|
||||
$target_arg = escapeshellarg(FileSystem::convertPath($target_path));
|
||||
|
||||
$mute = $this->console_putput ? '' : ' > NUL';
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[7Z EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
}
|
||||
}
|
||||
259
src/StaticPHP/Runtime/Shell/Shell.php
Normal file
259
src/StaticPHP/Runtime/Shell/Shell.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\ExecutionException;
|
||||
|
||||
abstract class Shell
|
||||
{
|
||||
protected ?string $cd = null;
|
||||
|
||||
protected bool $console_putput;
|
||||
|
||||
protected array $env = [];
|
||||
|
||||
protected string $last_cmd = '';
|
||||
|
||||
protected readonly bool $enable_log_file;
|
||||
|
||||
protected static mixed $passthru_callback = null;
|
||||
|
||||
public function __construct(?bool $console_output = null, bool $enable_log_file = true)
|
||||
{
|
||||
$this->console_putput = $console_output ?? ApplicationContext::isDebug();
|
||||
$this->enable_log_file = $enable_log_file;
|
||||
}
|
||||
|
||||
public static function passthruCallback(?callable $callback): void
|
||||
{
|
||||
static::$passthru_callback = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to `cd` command in shell.
|
||||
*
|
||||
* @param string $dir Directory to change to
|
||||
*/
|
||||
public function cd(string $dir): static
|
||||
{
|
||||
logger()->debug('Entering dir: ' . $dir);
|
||||
$c = clone $this;
|
||||
$c->cd = $dir;
|
||||
return $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set temporarily defined environment variables for current shell commands.
|
||||
*
|
||||
* @param array<string, string> $env Environment variables sets
|
||||
*/
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
foreach ($env as $k => $v) {
|
||||
if (trim($v) === '') {
|
||||
continue;
|
||||
}
|
||||
$this->env[$k] = trim($v);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append temporarily defined environment variables for current shell commands.
|
||||
*
|
||||
* @param array<string, string> $env Environment variables sets
|
||||
*/
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
foreach ($env as $k => $v) {
|
||||
if ($v === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($this->env[$k])) {
|
||||
$this->env[$k] = $v;
|
||||
} else {
|
||||
$this->env[$k] = "{$v} {$this->env[$k]}";
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command in the shell.
|
||||
*/
|
||||
abstract public function exec(string $cmd): static;
|
||||
|
||||
/**
|
||||
* Returns the last executed command.
|
||||
*/
|
||||
public function getLastCommand(): string
|
||||
{
|
||||
return $this->last_cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns unix-style environment variable string.
|
||||
*/
|
||||
public function getEnvString(): string
|
||||
{
|
||||
$str = '';
|
||||
foreach ($this->env as $k => $v) {
|
||||
$str .= ' ' . $k . '="' . $v . '"';
|
||||
}
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the command information to a log file.
|
||||
*/
|
||||
protected function logCommandInfo(string $cmd): void
|
||||
{
|
||||
if (!$this->enable_log_file) {
|
||||
return;
|
||||
}
|
||||
// write executed command to log file using fwrite
|
||||
$log_file = fopen(SPC_SHELL_LOG, 'a');
|
||||
fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n");
|
||||
fwrite($log_file, "> Executing command: {$cmd}\n");
|
||||
// get the backtrace to find the file and line number
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
|
||||
if (isset($backtrace[1]['file'], $backtrace[1]['line'])) {
|
||||
$file = $backtrace[1]['file'];
|
||||
$line = $backtrace[1]['line'];
|
||||
fwrite($log_file, "> Called from: {$file} at line {$line}\n");
|
||||
}
|
||||
fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n");
|
||||
if ($this->cd !== null) {
|
||||
fwrite($log_file, "> Working dir: {$this->cd}\n");
|
||||
}
|
||||
fwrite($log_file, "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// write executed command to the log file using fwrite
|
||||
$file_res = fopen(SPC_SHELL_LOG, 'a');
|
||||
}
|
||||
if ($console_output) {
|
||||
$console_res = STDOUT;
|
||||
}
|
||||
$descriptors = [
|
||||
0 => ['file', 'php://stdin', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'], // stderr
|
||||
];
|
||||
$process = proc_open($cmd, $descriptors, $pipes);
|
||||
|
||||
$output_value = '';
|
||||
try {
|
||||
if (!is_resource($process)) {
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: 'Failed to open process for command, proc_open() failed.',
|
||||
code: -1,
|
||||
cd: $this->cd,
|
||||
env: $this->env
|
||||
);
|
||||
}
|
||||
// fclose($pipes[0]);
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
while (true) {
|
||||
$status = proc_get_status($process);
|
||||
if (!$status['running']) {
|
||||
foreach ([$pipes[1], $pipes[2]] as $pipe) {
|
||||
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
||||
if ($console_output) {
|
||||
fwrite($console_res, $chunk);
|
||||
}
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, $chunk);
|
||||
}
|
||||
if ($capture_output) {
|
||||
$output_value .= $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
// check exit code
|
||||
if ($throw_on_error && $status['exitcode'] !== 0) {
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n");
|
||||
}
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: "Command exited with non-zero code: {$status['exitcode']}",
|
||||
code: $status['exitcode'],
|
||||
cd: $this->cd,
|
||||
env: $this->env,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (static::$passthru_callback !== null) {
|
||||
$callback = static::$passthru_callback;
|
||||
$callback();
|
||||
}
|
||||
$read = [$pipes[1], $pipes[2]];
|
||||
$write = null;
|
||||
$except = null;
|
||||
|
||||
$ready = stream_select($read, $write, $except, 0, 100000);
|
||||
|
||||
if ($ready === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ready > 0) {
|
||||
foreach ($read as $pipe) {
|
||||
while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') {
|
||||
if ($console_output) {
|
||||
fwrite($console_res, $chunk);
|
||||
}
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, $chunk);
|
||||
}
|
||||
if ($capture_output) {
|
||||
$output_value .= $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $status['exitcode'],
|
||||
'output' => $output_value,
|
||||
];
|
||||
} finally {
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
if ($file_res !== null) {
|
||||
fclose($file_res);
|
||||
}
|
||||
proc_close($process);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/StaticPHP/Runtime/Shell/UnixShell.php
Normal file
91
src/StaticPHP/Runtime/Shell/UnixShell.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
/**
|
||||
* Unix-like OS shell command executor.
|
||||
*
|
||||
* This class provides methods to execute shell commands in a Unix-like environment.
|
||||
* It supports setting environment variables and changing the working directory.
|
||||
*/
|
||||
class UnixShell extends Shell
|
||||
{
|
||||
public function __construct(?bool $console_output = null)
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
throw new SPCInternalException('Windows cannot use UnixShell');
|
||||
}
|
||||
parent::__construct($console_output);
|
||||
}
|
||||
|
||||
public function exec(string $cmd): static
|
||||
{
|
||||
$cmd = clean_spaces($cmd);
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd));
|
||||
$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);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the environment variable that common build will be used.
|
||||
*
|
||||
* @param LibraryPackage $library Library package
|
||||
*/
|
||||
public function initializeEnv(LibraryPackage $library): UnixShell
|
||||
{
|
||||
$this->setEnv([
|
||||
'CFLAGS' => $library->getLibExtraCFlags(),
|
||||
'CXXFLAGS' => $library->getLibExtraCXXFlags(),
|
||||
'LDFLAGS' => $library->getLibExtraLdFlags(),
|
||||
'LIBS' => $library->getLibExtraLibs() . SystemTarget::getRuntimeLibs(),
|
||||
]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command and return the result.
|
||||
*
|
||||
* @param string $cmd Command to execute
|
||||
* @param bool $with_log Whether to log the command
|
||||
* @return array{0: int, 1: string[]} Return code and output lines
|
||||
*/
|
||||
public function execWithResult(string $cmd, bool $with_log = true): array
|
||||
{
|
||||
if ($with_log) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd));
|
||||
} else {
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd));
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
$this->logCommandInfo($cmd);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
|
||||
private function getExecString(string $cmd): string
|
||||
{
|
||||
// logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']);
|
||||
$env_str = $this->getEnvString();
|
||||
if (!empty($env_str)) {
|
||||
$cmd = "{$env_str} {$cmd}";
|
||||
}
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
156
src/StaticPHP/Runtime/Shell/WindowsCmd.php
Normal file
156
src/StaticPHP/Runtime/Shell/WindowsCmd.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\Exception\ExecutionException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
class WindowsCmd extends Shell
|
||||
{
|
||||
public function __construct(?bool $debug = null)
|
||||
{
|
||||
if (PHP_OS_FAMILY !== 'Windows') {
|
||||
throw new SPCInternalException('Only windows can use WindowsCmd');
|
||||
}
|
||||
parent::__construct($debug);
|
||||
}
|
||||
|
||||
public function exec(string $cmd): static
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd));
|
||||
|
||||
$original_command = $cmd;
|
||||
$this->logCommandInfo($original_command);
|
||||
$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);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function execWithWrapper(string $wrapper, string $args): WindowsCmd
|
||||
{
|
||||
return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"');
|
||||
}
|
||||
|
||||
public function execWithResult(string $cmd, bool $with_log = true): array
|
||||
{
|
||||
if ($with_log) {
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd));
|
||||
} else {
|
||||
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);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
// windows currently does not support setting environment variables
|
||||
throw new SPCInternalException('Windows does not support setting environment variables in shell commands.');
|
||||
}
|
||||
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
// windows currently does not support appending environment variables
|
||||
throw new SPCInternalException('Windows does not support appending environment variables in shell commands.');
|
||||
}
|
||||
|
||||
public function getLastCommand(): string
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
132
src/StaticPHP/Runtime/SystemTarget.php
Normal file
132
src/StaticPHP/Runtime/SystemTarget.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime;
|
||||
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
/**
|
||||
* Originally from SPCTarget, used to offer some build-time information about the target.
|
||||
*/
|
||||
class SystemTarget
|
||||
{
|
||||
/**
|
||||
* Returns the libc type if set, for other OS, it will always return null.
|
||||
*/
|
||||
public static function getLibc(): ?string
|
||||
{
|
||||
if ($target = getenv('SPC_TARGET')) {
|
||||
if (str_contains($target, '-gnu')) {
|
||||
return 'glibc';
|
||||
}
|
||||
if (str_contains($target, '-musl')) {
|
||||
return 'musl';
|
||||
}
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
return LinuxUtil::isMuslDist() ? 'musl' : 'glibc';
|
||||
}
|
||||
}
|
||||
$libc = getenv('SPC_LIBC');
|
||||
if ($libc !== false) {
|
||||
return $libc;
|
||||
}
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
return LinuxUtil::isMuslDist() ? 'musl' : 'glibc';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system runtime libraries linker flags.
|
||||
*/
|
||||
public static function getRuntimeLibs(): string
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
return self::getLibc() === 'musl' ? '-ldl -lpthread -lm' : '-ldl -lrt -lpthread -lm -lresolv -lutil';
|
||||
}
|
||||
if (PHP_OS_FAMILY === 'Darwin') {
|
||||
return '-lresolv';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the libc version if set, for other OS, it will always return null.
|
||||
*/
|
||||
public static function getLibcVersion(): ?string
|
||||
{
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
$target = (string) getenv('SPC_TARGET');
|
||||
if (str_contains($target, '-gnu.2.')) {
|
||||
return preg_match('/-gnu\.(2\.\d+)/', $target, $matches) ? $matches[1] : null;
|
||||
}
|
||||
$libc = self::getLibc();
|
||||
return LinuxUtil::getLibcVersionIfExists($libc);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target OS family, e.g. Linux, Darwin, Windows, BSD.
|
||||
* Currently, we only support native building.
|
||||
*
|
||||
* @return 'BSD'|'Darwin'|'Linux'|'Windows'
|
||||
*/
|
||||
public static function getTargetOS(): string
|
||||
{
|
||||
$target = (string) getenv('SPC_TARGET');
|
||||
return match (true) {
|
||||
str_contains($target, '-linux') => 'Linux',
|
||||
str_contains($target, '-macos') => 'Darwin',
|
||||
str_contains($target, '-windows') => 'Windows',
|
||||
str_contains($target, '-native') => PHP_OS_FAMILY,
|
||||
default => PHP_OS_FAMILY,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target architecture, e.g. x86_64, aarch64.
|
||||
* Currently, we only support 'x86_64' and 'aarch64' and both can only be built natively.
|
||||
*
|
||||
* @return 'aarch64'|'x86_64'
|
||||
*/
|
||||
public static function getTargetArch(): string
|
||||
{
|
||||
$target = (string) getenv('SPC_TARGET');
|
||||
return match (true) {
|
||||
str_contains($target, 'x86_64') || str_contains($target, 'amd64') => 'x86_64',
|
||||
str_contains($target, 'aarch64') || str_contains($target, 'arm64') => 'aarch64',
|
||||
// str_contains($target, 'armv7') || str_contains($target, 'armhf') => 'armv7',
|
||||
// str_contains($target, 'armv6') || str_contains($target, 'armel') => 'armv6',
|
||||
// str_contains($target, 'i386') || str_contains($target, 'i686') => 'i386',
|
||||
default => GNU_ARCH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current platform string in the format of {os}-{arch}, e.g. linux-x86_64.
|
||||
*/
|
||||
public static function getCurrentPlatformString(): string
|
||||
{
|
||||
$os = match (self::getTargetOS()) {
|
||||
'Darwin' => 'macos',
|
||||
'Linux' => 'linux',
|
||||
'Windows' => 'windows',
|
||||
default => 'unknown',
|
||||
};
|
||||
$arch = self::getTargetArch();
|
||||
if (getenv('EMULATE_PLATFORM') !== false) {
|
||||
return getenv('EMULATE_PLATFORM');
|
||||
}
|
||||
return "{$os}-{$arch}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the target OS is a Unix-like system.
|
||||
*/
|
||||
public static function isUnix(): bool
|
||||
{
|
||||
return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user