Files
static-php-cli/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php
2026-06-16 19:05:09 +07:00

229 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace StaticPHP\Runtime\Executor;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Runtime\Shell\WindowsCmd;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\System\WindowsUtil;
use ZM\Logger\ConsoleColor;
class WindowsCMakeExecutor extends Executor
{
protected WindowsCmd $cmd;
protected array $configure_args = [];
protected array $ignore_args = [];
protected ?string $build_dir = null;
protected ?array $custom_default_args = null;
protected int $steps = 2;
protected bool $reset = true;
protected PackageBuilder $builder;
protected PackageInstaller $installer;
public function __construct(protected LibraryPackage $package)
{
parent::__construct($this->package);
$this->builder = ApplicationContext::get(PackageBuilder::class);
$this->installer = ApplicationContext::get(PackageInstaller::class);
$this->initCmd();
// 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.");
}
}
public function build(): static
{
$this->initBuildDir();
if ($this->reset) {
FileSystem::resetDir($this->build_dir);
}
// configure
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->cmd->exec("cmake {$configure_args}");
}
// make
if ($this->steps >= 2) {
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake build)'));
$this->cmd->cd($this->build_dir)->exec("cmake --build {$this->build_dir} --config Release --target install -j{$this->builder->concurrency}");
}
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(string ...$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(string ...$args): static
{
$this->ignore_args = [...$this->ignore_args, ...$args];
return $this;
}
public function setEnv(array $env): static
{
$this->cmd->setEnv($env);
return $this;
}
public function appendEnv(array $env): static
{
$this->cmd->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()));
}
public function setWorkingDir(string $dir): static
{
$this->cmd = $this->cmd->cd($dir);
return $this;
}
/**
* Returns the default CMake args.
*/
private function getDefaultCMakeArgs(): array
{
return $this->custom_default_args ?? [
'-A x64',
// CMake 4.x hard-errors on projects requesting compatibility with CMake < 3.5
// (e.g. wineditline). This is the documented escape hatch; modern projects and
// older CMake releases ignore it.
'-DCMAKE_POLICY_VERSION_MINIMUM=3.5',
'-DCMAKE_BUILD_TYPE=Release',
'-DBUILD_SHARED_LIBS=OFF',
'-DBUILD_STATIC_LIBS=ON',
"-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}",
'-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($this->package->getBuildRootPath()),
'-B ' . escapeshellarg(FileSystem::convertPath($this->build_dir)),
];
}
private function makeCmakeToolchainFile(): string
{
if (file_exists(SOURCE_PATH . '\toolchain.cmake')) {
return SOURCE_PATH . '\toolchain.cmake';
}
return WindowsUtil::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->getSourceRoot()}\\build";
}
}
private function initCmd(): void
{
$this->cmd = cmd()->cd($this->package->getSourceRoot());
}
}