diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f7138d01..fe25afa0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -114,6 +114,7 @@ jobs: os: - ubuntu-latest - macos-latest + - windows-latest fail-fast: false steps: - name: "Checkout" diff --git a/README-zh.md b/README-zh.md index 0da16a73..68c5e82c 100755 --- a/README-zh.md +++ b/README-zh.md @@ -254,14 +254,13 @@ bin/spc micro:combine my-app.phar -I "memory_limit=4G" -I "disable_functions=sys ## 开源协议 -本项目依据旧版本惯例采用 MIT License 开源,部分扩展的集成编译命令参考或修改自以下项目: +本项目采用 MIT License 许可开源,下面是类似的项目: -- [dixyes/lwmbs](https://github.com/dixyes/lwmbs)(木兰宽松许可证) -- [swoole/swoole-cli](https://github.com/swoole/swoole-cli)(Apache 2.0 LICENSE、SWOOLE-CLI LICENSE) +- [dixyes/lwmbs](https://github.com/dixyes/lwmbs) +- [swoole/swoole-cli](https://github.com/swoole/swoole-cli) + +该项目使用了 [dixyes/lwmbs](https://github.com/dixyes/lwmbs) 中的一些代码,例如 Windows 静态构建目标和 libiconv 库支持。 +lwmbs 使用 [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2) 许可进行分发。对应文件有关于作者和许可的特殊说明,除此之外,均使用 MIT 授权许可。 因本项目的特殊性,使用项目编译过程中会使用很多其他开源项目,例如 curl、protobuf 等,它们都有各自的开源协议。 请在编译完成后,使用命令 `bin/spc dump-license` 导出项目使用项目的开源协议,并遵守对应项目的 LICENSE。 - -## 进阶 - -本项目重构分支为模块化编写。如果你对本项目感兴趣,想加入开发,可以参照文档的 [贡献指南](https://static-php.dev) 贡献代码或文档。 diff --git a/README.md b/README.md index 1039ce84..83fd4005 100755 --- a/README.md +++ b/README.md @@ -287,6 +287,9 @@ These are similar projects: - [dixyes/lwmbs](https://github.com/dixyes/lwmbs) - [swoole/swoole-cli](https://github.com/swoole/swoole-cli) +The project uses some code from [dixyes/lwmbs](https://github.com/dixyes/lwmbs), such as windows static build target and libiconv support. +lwmbs is licensed under the [Mulan PSL 2](http://license.coscl.org.cn/MulanPSL2). + Due to the special nature of this project, many other open source projects such as curl and protobuf will be used during the project compilation process, and they all have their own open source licenses. diff --git a/bin/setup-runtime.ps1 b/bin/setup-runtime.ps1 index b04c3b58..e9c152dd 100644 --- a/bin/setup-runtime.ps1 +++ b/bin/setup-runtime.ps1 @@ -28,7 +28,7 @@ function RemoveFromPath { $currentPath = [System.Environment]::GetEnvironmentVariable('Path', 'User') if ($currentPath -like "*$pathToRemove*") { - $newPath = $currentPath -replace [regex]::Escape($pathToRemove), '' + $newPath = $currentPath -replace [regex]::Escape(';' + $pathToRemove), '' [System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User') Write-Host "Removed Path '$pathToRemove'" } else { @@ -92,7 +92,7 @@ if (-not(Test-Path "runtime\composer.phar")) # create runtime\composer.ps1 $ComposerContent = ' $WorkingDir = (Split-Path -Parent $MyInvocation.MyCommand.Definition) -Start-Process ($WorkingDir + "\php.exe") ($WorkingDir + "\composer.phar " + $args) -NoNewWindow -Wait +& ($WorkingDir + "\php.exe") (Join-Path $WorkingDir "\composer.phar") @args ' $ComposerContent | Set-Content -Path 'runtime\composer.ps1' -Encoding UTF8 diff --git a/bin/spc b/bin/spc index a8571b1b..52a6401b 100755 --- a/bin/spc +++ b/bin/spc @@ -21,4 +21,5 @@ try { (new ConsoleApplication())->run(); } catch (Exception $e) { ExceptionHandler::getInstance()->handle($e); + exit(1); } diff --git a/bin/spc.ps1 b/bin/spc.ps1 index 7123a880..3a2639d0 100644 --- a/bin/spc.ps1 +++ b/bin/spc.ps1 @@ -1,4 +1,4 @@ -$PHP_Exec = "runtime\php.exe" +$PHP_Exec = ".\runtime\php.exe" if (-not(Test-Path $PHP_Exec)) { $PHP_Exec = Get-Command php.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Definition @@ -8,5 +8,4 @@ if (-not(Test-Path $PHP_Exec)) { } } -$phpArgs = "bin\spc " + $args -Start-Process $PHP_Exec -ArgumentList $phpArgs -NoNewWindow -Wait +& "$PHP_Exec" ("bin/spc") @args diff --git a/config/ext.json b/config/ext.json index 2dfc67d6..3ee4cd32 100644 --- a/config/ext.json +++ b/config/ext.json @@ -106,7 +106,8 @@ "source": "ext-glfw", "lib-depends": [ "glfw" - ] + ], + "lib-depends-windows": [] }, "gmp": { "type": "builtin", @@ -238,6 +239,7 @@ "openssl": { "type": "builtin", "arg-type": "custom", + "arg-type-windows": "with", "lib-depends": [ "openssl", "zlib" diff --git a/config/lib.json b/config/lib.json index a56d8772..d9400b76 100644 --- a/config/lib.json +++ b/config/lib.json @@ -57,9 +57,7 @@ "brotli", "nghttp2", "zstd", - "openssl", - "idn2", - "psl" + "openssl" ], "frameworks": [ "CoreFoundation", diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index a067e88d..19cf3083 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -23,7 +23,7 @@ use Symfony\Component\Console\Command\ListCommand; */ final class ConsoleApplication extends Application { - public const VERSION = '2.0.1'; + public const VERSION = '2.1.0-beta.1'; public function __construct() { diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index 729a46c5..531d8ea3 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -9,10 +9,8 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; -use SPC\store\FileSystem; use SPC\store\SourceExtractor; use SPC\util\CustomExt; -use SPC\util\DependencyUtil; abstract class BuilderBase { @@ -43,64 +41,7 @@ abstract class BuilderBase * @throws WrongUsageException * @internal */ - public function buildLibs(array $sorted_libraries): void - { - // search all supported libs - $support_lib_list = []; - $classes = FileSystem::getClassesPsr4( - ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library', - 'SPC\\builder\\' . osfamily2dir() . '\\library' - ); - foreach ($classes as $class) { - if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) { - $support_lib_list[$class::NAME] = $class; - } - } - - // if no libs specified, compile all supported libs - if ($sorted_libraries === [] && $this->isLibsOnly()) { - $libraries = array_keys($support_lib_list); - $sorted_libraries = DependencyUtil::getLibsByDeps($libraries); - } - - // pkg-config must be compiled first, whether it is specified or not - if (!in_array('pkg-config', $sorted_libraries)) { - array_unshift($sorted_libraries, 'pkg-config'); - } - - // add lib object for builder - foreach ($sorted_libraries as $library) { - // if some libs are not supported (but in config "lib.json", throw exception) - if (!isset($support_lib_list[$library])) { - throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!'); - } - $lib = new ($support_lib_list[$library])($this); - $this->addLib($lib); - } - - // calculate and check dependencies - foreach ($this->libs as $lib) { - $lib->calcDependency(); - } - - // patch point - $this->emitPatchPoint('before-libs-extract'); - - // extract sources - SourceExtractor::initSource(libs: $sorted_libraries); - - $this->emitPatchPoint('after-libs-extract'); - - // build all libs - foreach ($this->libs as $lib) { - match ($lib->tryBuild($this->getOption('rebuild', false))) { - BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'), - BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), - BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'), - default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), - }; - } - } + abstract public function buildLibs(array $sorted_libraries); /** * Add library to build. @@ -242,6 +183,7 @@ abstract class BuilderBase { $ret = []; foreach ($this->exts as $ext) { + logger()->info($ext->getName() . ' is using ' . $ext->getConfigureArg()); $ret[] = trim($ext->getConfigureArg()); } logger()->debug('Using configure: ' . implode(' ', $ret)); @@ -417,4 +359,20 @@ abstract class BuilderBase ); } } + + /** + * Generate micro extension test php code. + */ + protected function generateMicroExtTests(): string + { + $php = "getExts() as $ext) { + $ext_name = $ext->getDistName(); + $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; + $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; + } + $php .= "echo '[micro-test-end]';\n"; + return $php; + } } diff --git a/src/SPC/builder/BuilderProvider.php b/src/SPC/builder/BuilderProvider.php index d1e0cd77..fb6d3cef 100644 --- a/src/SPC/builder/BuilderProvider.php +++ b/src/SPC/builder/BuilderProvider.php @@ -7,6 +7,7 @@ namespace SPC\builder; use SPC\builder\freebsd\BSDBuilder; use SPC\builder\linux\LinuxBuilder; use SPC\builder\macos\MacOSBuilder; +use SPC\builder\windows\WindowsBuilder; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; @@ -27,11 +28,7 @@ class BuilderProvider public static function makeBuilderByInput(InputInterface $input): BuilderBase { self::$builder = match (PHP_OS_FAMILY) { - // 'Windows' => new WindowsBuilder( - // binary_sdk_dir: $input->getOption('with-sdk-binary-dir'), - // vs_ver: $input->getOption('vs-ver'), - // arch: $input->getOption('arch'), - // ), + 'Windows' => new WindowsBuilder($input->getOptions()), 'Darwin' => new MacOSBuilder($input->getOptions()), 'Linux' => new LinuxBuilder($input->getOptions()), 'BSD' => new BSDBuilder($input->getOptions()), diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 21f0f574..c5c026ff 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -8,6 +8,7 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; +use SPC\store\FileSystem; class Extension { @@ -42,7 +43,7 @@ class Extension $arg = $this->getEnableArg(); switch (PHP_OS_FAMILY) { case 'Windows': - $arg = $this->getWindowsConfigureArg(); + $arg .= $this->getWindowsConfigureArg(); break; case 'Darwin': case 'Linux': @@ -164,14 +165,13 @@ class Extension } /** - * Run compile check if build target is cli - * If you need to run some check, overwrite this or add your assert in src/globals/tests/{extension_name}.php - * If check failed, throw RuntimeException - * * @throws RuntimeException */ - public function runCliCheck(): void + public function runCliCheckUnix(): void { + // Run compile check if build target is cli + // If you need to run some check, overwrite this or add your assert in src/globals/tests/{extension_name}.php + // If check failed, throw RuntimeException [$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $this->getDistName() . '"', false); if ($ret !== 0) { throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret); @@ -192,6 +192,34 @@ class Extension } } + /** + * @throws RuntimeException + */ + public function runCliCheckWindows(): void + { + // Run compile check if build target is cli + // If you need to run some check, overwrite this or add your assert in src/globals/tests/{extension_name}.php + // If check failed, throw RuntimeException + [$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe --ri "' . $this->getDistName() . '"', false); + if ($ret !== 0) { + throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret); + } + + if (file_exists(FileSystem::convertPath(ROOT_DIR . '/src/globals/tests/' . $this->getName() . '.php'))) { + // Trim additional content & escape special characters to allow inline usage + $test = str_replace( + ['getName() . '.php')) + ); + + [$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check'); + } + } + } + /** * @throws RuntimeException */ diff --git a/src/SPC/builder/extension/glfw.php b/src/SPC/builder/extension/glfw.php index ad1be3a7..c9d0c4f7 100644 --- a/src/SPC/builder/extension/glfw.php +++ b/src/SPC/builder/extension/glfw.php @@ -34,4 +34,9 @@ class glfw extends Extension { return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH; } + + public function getWindowsConfigureArg(): string + { + return '--enable-glfw=static'; + } } diff --git a/src/SPC/builder/extension/mbregex.php b/src/SPC/builder/extension/mbregex.php index 0301557c..8eb40615 100644 --- a/src/SPC/builder/extension/mbregex.php +++ b/src/SPC/builder/extension/mbregex.php @@ -19,7 +19,7 @@ class mbregex extends Extension /** * mbregex is not an extension, we need to overwrite the default check. */ - public function runCliCheck(): void + public function runCliCheckUnix(): void { [$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring" | grep regex', false); if ($ret !== 0) { diff --git a/src/SPC/builder/extension/password_argon2.php b/src/SPC/builder/extension/password_argon2.php index fb3c4b04..db1ada4e 100644 --- a/src/SPC/builder/extension/password_argon2.php +++ b/src/SPC/builder/extension/password_argon2.php @@ -11,7 +11,7 @@ use SPC\util\CustomExt; #[CustomExt('password-argon2')] class password_argon2 extends Extension { - public function runCliCheck(): void + public function runCliCheckUnix(): void { [$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "assert(defined(\'PASSWORD_ARGON2I\'));"'); if ($ret !== 0) { diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php index b1fa8851..15906bea 100644 --- a/src/SPC/builder/freebsd/BSDBuilder.php +++ b/src/SPC/builder/freebsd/BSDBuilder.php @@ -4,19 +4,15 @@ declare(strict_types=1); namespace SPC\builder\freebsd; -use SPC\builder\BuilderBase; -use SPC\builder\traits\UnixBuilderTrait; +use SPC\builder\unix\UnixBuilderBase; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\SourcePatcher; -class BSDBuilder extends BuilderBase +class BSDBuilder extends UnixBuilderBase { - /** Unix compatible builder methods */ - use UnixBuilderTrait; - /** @var bool Micro patch phar flag */ private bool $phar_patched = false; diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index f99c9bca..2fe93534 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -4,20 +4,16 @@ declare(strict_types=1); namespace SPC\builder\linux; -use SPC\builder\BuilderBase; use SPC\builder\linux\library\LinuxLibraryBase; -use SPC\builder\traits\UnixBuilderTrait; +use SPC\builder\unix\UnixBuilderBase; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\SourcePatcher; -class LinuxBuilder extends BuilderBase +class LinuxBuilder extends UnixBuilderBase { - /** Unix compatible builder methods */ - use UnixBuilderTrait; - /** @var array Tune cflags */ public array $tune_c_flags; diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php index b74aaa60..def7d8b0 100644 --- a/src/SPC/builder/macos/MacOSBuilder.php +++ b/src/SPC/builder/macos/MacOSBuilder.php @@ -4,20 +4,16 @@ declare(strict_types=1); namespace SPC\builder\macos; -use SPC\builder\BuilderBase; use SPC\builder\macos\library\MacOSLibraryBase; -use SPC\builder\traits\UnixBuilderTrait; +use SPC\builder\unix\UnixBuilderBase; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\SourcePatcher; -class MacOSBuilder extends BuilderBase +class MacOSBuilder extends UnixBuilderBase { - /** Unix compatible builder methods */ - use UnixBuilderTrait; - /** @var bool Micro patch phar flag */ private bool $phar_patched = false; diff --git a/src/SPC/builder/traits/LibraryTrait.php b/src/SPC/builder/traits/LibraryTrait.php deleted file mode 100644 index e05a47bf..00000000 --- a/src/SPC/builder/traits/LibraryTrait.php +++ /dev/null @@ -1,7 +0,0 @@ -isLibsOnly()) { + $libraries = array_keys($support_lib_list); + $sorted_libraries = DependencyUtil::getLibsByDeps($libraries); + } + + // pkg-config must be compiled first, whether it is specified or not + if (!in_array('pkg-config', $sorted_libraries)) { + array_unshift($sorted_libraries, 'pkg-config'); + } + + // add lib object for builder + foreach ($sorted_libraries as $library) { + // if some libs are not supported (but in config "lib.json", throw exception) + if (!isset($support_lib_list[$library])) { + throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!'); + } + $lib = new ($support_lib_list[$library])($this); + $this->addLib($lib); + } + + // calculate and check dependencies + foreach ($this->libs as $lib) { + $lib->calcDependency(); + } + + // patch point + $this->emitPatchPoint('before-libs-extract'); + + // extract sources + SourceExtractor::initSource(libs: $sorted_libraries); + + $this->emitPatchPoint('after-libs-extract'); + + // build all libs + foreach ($this->libs as $lib) { + match ($lib->tryBuild($this->getOption('rebuild', false))) { + BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'), + BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), + BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'), + default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), + }; + } + } + /** * Sanity check after build complete * @@ -103,7 +166,7 @@ trait UnixBuilderTrait foreach ($this->exts as $ext) { logger()->debug('testing ext: ' . $ext->getName()); - $ext->runCliCheck(); + $ext->runCliCheckUnix(); } } diff --git a/src/SPC/builder/windows/SystemUtil.php b/src/SPC/builder/windows/SystemUtil.php index b1ec35af..668e60b1 100644 --- a/src/SPC/builder/windows/SystemUtil.php +++ b/src/SPC/builder/windows/SystemUtil.php @@ -4,17 +4,25 @@ declare(strict_types=1); namespace SPC\builder\windows; +use SPC\exception\FileSystemException; +use SPC\store\FileSystem; + class SystemUtil { /** - * @param string $name 命令名称 - * @param array $paths 寻找的目标路径(如果不传入,则使用环境变量 PATH) - * @return null|string 找到了返回命令路径,找不到返回 null + * Find windows program using executable name. + * + * @param string $name command name (xxx.exe) + * @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 = []): ?string + public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string { if (!$paths) { $paths = explode(PATH_SEPARATOR, getenv('Path')); + if ($include_sdk_bin) { + $paths[] = PHP_SDK_PATH . '\bin'; + } } foreach ($paths as $path) { if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { @@ -23,4 +31,74 @@ class SystemUtil } return null; } + + /** + * Find Visual Studio installation. + * + * @return array|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', + ]; + 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, + ]; + } + } + return false; + } + + /** + * Get CPU count for concurrency. + */ + public static function getCpuCount(): int + { + $result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code); + if ($code !== 0 || !$result) { + return 1; + } + return intval($result); + } + + /** + * Create CMake toolchain file. + * + * @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=' + * @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt' + * @throws FileSystemException + */ + public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string + { + if ($cflags === null) { + $cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='; + } + if ($ldflags === null) { + $ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'; + } + $buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH); + $toolchain = <<options = $options; + + // ---------- set necessary options ---------- + // set sdk (require visual studio 16 or 17) + $vs = SystemUtil::findVisualStudio()['version']; + $this->sdk_prefix = PHP_SDK_PATH . "\\phpsdk-{$vs}-x64.bat -t"; + + // set zts + $this->zts = $this->getOption('enable-zts', false); + + // set concurrency + $this->concurrency = SystemUtil::getCpuCount(); + + // make cmake toolchain + $this->cmake_toolchain_file = SystemUtil::makeCmakeToolchainFile(); + } + + /** + * @throws RuntimeException + * @throws WrongUsageException + * @throws FileSystemException + */ + public function buildPHP(int $build_target = BUILD_TARGET_NONE): void + { + // ---------- Update extra-libs ---------- + $extra_libs = $this->getOption('extra-libs', ''); + $extra_libs .= (empty($extra_libs) ? '' : ' ') . implode(' ', $this->getAllStaticLibFiles()); + $this->setOption('extra-libs', $extra_libs); + $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; + $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; + $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; + $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + + SourcePatcher::patchBeforeBuildconf($this); + + cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} buildconf.bat"); + + SourcePatcher::patchBeforeConfigure($this); + + $zts = $this->zts ? '--enable-zts=yes ' : '--enable-zts=no '; + + cmd()->cd(SOURCE_PATH . '\php-src') + ->exec( + "{$this->sdk_prefix} configure.bat --task-args \"" . + '--disable-all ' . + '--disable-cgi ' . + '--with-php-build=' . BUILD_ROOT_PATH . ' ' . + '--with-extra-includes=' . BUILD_INCLUDE_PATH . ' ' . + '--with-extra-libs=' . BUILD_LIB_PATH . ' ' . + ($enableCli ? '--enable-cli=yes ' : '--enable-cli=no ') . + ($enableMicro ? '--enable-micro=yes ' : '--enable-micro=no ') . + ($enableEmbed ? '--enable-embed=yes ' : '--enable-embed=no ') . + "{$this->makeExtensionArgs()} " . + $zts . + '"' + ); + + SourcePatcher::patchBeforeMake($this); + + $this->cleanMake(); + + if ($enableCli) { + logger()->info('building cli'); + $this->buildCli(); + } + if ($enableFpm) { + logger()->warning('Windows does not support fpm SAPI, I will skip it.'); + } + if ($enableMicro) { + logger()->info('building micro'); + $this->buildMicro(); + } + if ($enableEmbed) { + logger()->warning('Windows does not currently support embed SAPI.'); + // logger()->info('building embed'); + $this->buildEmbed(); + } + + $this->sanityCheck($build_target); + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function buildCli(): void + { + SourcePatcher::patchWindowsCLITarget(); + + // add nmake wrapper + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cli_wrapper.bat', "nmake /nologo LIBS_CLI=\"{$this->getOption('extra-libs')} ws2_32.lib shell32.lib\" EXTRA_LD_FLAGS_PROGRAM= %*"); + + cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cli_wrapper.bat --task-args php.exe"); + + $this->deployBinary(BUILD_TARGET_CLI); + } + + public function buildEmbed(): void + { + // TODO: add embed support for windows + /* + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_embed_wrapper.bat', 'nmake /nologo %*'); + + cmd()->cd(SOURCE_PATH . '\php-src') + ->exec("{$this->sdk_prefix} nmake_embed_wrapper.bat --task-args php8embed.lib"); + */ + } + + /** + * @throws FileSystemException + * @throws RuntimeException + * @throws WrongUsageException + */ + public function buildMicro(): void + { + // workaround for fiber (originally from https://github.com/dixyes/lwmbs/blob/master/windows/MicroBuild.php) + $makefile = FileSystem::readFile(SOURCE_PATH . '\php-src\Makefile'); + if ($this->getPHPVersionID() >= 80200 && str_contains($makefile, 'FIBER_ASM_ARCH')) { + $makefile .= "\r\n" . '$(MICRO_SFX): $(BUILD_DIR)\Zend\jump_$(FIBER_ASM_ARCH)_ms_pe_masm.obj $(BUILD_DIR)\Zend\make_$(FIBER_ASM_ARCH)_ms_pe_masm.obj' . "\r\n\r\n"; + } + FileSystem::writeFile(SOURCE_PATH . '\php-src\Makefile', $makefile); + + // add nmake wrapper + $fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI" ' : ''; + $wrapper = "nmake /nologo LIBS_MICRO=\"{$this->getOption('extra-libs')} ws2_32.lib shell32.lib\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" %*"; + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_micro_wrapper.bat', $wrapper); + + // phar patch for micro + if ($this->getExt('phar')) { + $this->phar_patched = true; + SourcePatcher::patchMicro(['phar']); + } + + cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_micro_wrapper.bat --task-args micro"); + + if ($this->phar_patched) { + SourcePatcher::patchMicro(['phar'], true); + } + + $this->deployBinary(BUILD_TARGET_MICRO); + } + + public function buildLibs(array $sorted_libraries): void + { + // search all supported libs + $support_lib_list = []; + $classes = FileSystem::getClassesPsr4( + ROOT_DIR . '\src\SPC\builder\\' . osfamily2dir() . '\\library', + 'SPC\\builder\\' . osfamily2dir() . '\\library' + ); + foreach ($classes as $class) { + if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) { + $support_lib_list[$class::NAME] = $class; + } + } + + // if no libs specified, compile all supported libs + if ($sorted_libraries === [] && $this->isLibsOnly()) { + $libraries = array_keys($support_lib_list); + $sorted_libraries = DependencyUtil::getLibsByDeps($libraries); + } + + // add lib object for builder + foreach ($sorted_libraries as $library) { + // if some libs are not supported (but in config "lib.json", throw exception) + if (!isset($support_lib_list[$library])) { + throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!'); + } + $lib = new ($support_lib_list[$library])($this); + $this->addLib($lib); + } + + // calculate and check dependencies + foreach ($this->libs as $lib) { + $lib->calcDependency(); + } + + // extract sources + SourceExtractor::initSource(libs: $sorted_libraries); + + // build all libs + foreach ($this->libs as $lib) { + match ($lib->tryBuild($this->getOption('rebuild', false))) { + BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'), + BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), + BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'), + default => logger()->warning('lib [' . $lib::NAME . '] build status unknown'), + }; + } + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function cleanMake(): void + { + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_clean_wrapper.bat', 'nmake /nologo %*'); + cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_clean_wrapper.bat --task-args \"clean\""); + } + + /** + * Run extension and PHP cli and micro check + * + * @throws RuntimeException + */ + public function sanityCheck(mixed $build_target): void + { + // sanity check for php-cli + if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { + logger()->info('running cli sanity check'); + [$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -r "echo \"hello\";"'); + if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { + throw new RuntimeException('cli failed sanity check'); + } + + foreach ($this->exts as $ext) { + logger()->debug('testing ext: ' . $ext->getName()); + $ext->runCliCheckWindows(); + } + } + + // sanity check for phpmicro + if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { + if (file_exists(SOURCE_PATH . '\hello.exe')) { + @unlink(SOURCE_PATH . '\hello.exe'); + } + file_put_contents( + SOURCE_PATH . '\hello.exe', + file_get_contents(BUILD_ROOT_PATH . '\bin\micro.sfx') . + ($this->getOption('with-micro-ext-test') ? $this->generateMicroExtTests() : 'execWithResult(SOURCE_PATH . '\hello.exe'); + $raw_out = trim(implode('', $output2)); + $condition[0] = $ret === 0; + $condition[1] = str_starts_with($raw_out, '[micro-test-start]') && str_ends_with($raw_out, '[micro-test-end]'); + foreach ($condition as $k => $v) { + if (!$v) { + throw new RuntimeException("micro failed sanity check with condition[{$k}], ret[{$ret}], out[{$raw_out}]"); + } + } + } + } + + /** + * 将编译好的二进制文件发布到 buildroot + * + * @param int $type 发布类型 + * @throws RuntimeException + * @throws FileSystemException + */ + public function deployBinary(int $type): bool + { + $ts = $this->zts ? '_TS' : ''; + $src = match ($type) { + BUILD_TARGET_CLI => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\php.exe", + BUILD_TARGET_MICRO => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\micro.sfx", + default => throw new RuntimeException('Deployment does not accept type ' . $type), + }; + logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file'); + FileSystem::createDir(BUILD_ROOT_PATH . '\bin'); + + cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '\bin\\')); + return true; + } + + /** + * @throws WrongUsageException + * @throws FileSystemException + */ + public function getAllStaticLibFiles(): array + { + $libs = []; + + // reorder libs + foreach ($this->libs as $lib) { + foreach ($lib->getDependencies() as $dep) { + $libs[] = $dep; + } + $libs[] = $lib; + } + + $libFiles = []; + $libNames = []; + // merge libs + foreach ($libs as $lib) { + if (!in_array($lib::NAME, $libNames, true)) { + $libNames[] = $lib::NAME; + array_unshift($libFiles, ...$lib->getStaticLibs()); + } + } + return $libFiles; + } + + /** + * Generate command wrapper prefix for php-sdk internal commands. + * + * @param string $internal_cmd command in php-sdk-tools\bin + * @return string Example: C:\php-sdk-tools\phpsdk-vs17-x64.bat -t source\cmake_wrapper.bat --task-args + */ + public function makeSimpleWrapper(string $internal_cmd): string + { + $wrapper_bat = SOURCE_PATH . '\\' . crc32($internal_cmd) . '_wrapper.bat'; + if (!file_exists($wrapper_bat)) { + file_put_contents($wrapper_bat, $internal_cmd . ' %*'); + } + return "{$this->sdk_prefix} {$wrapper_bat} --task-args"; + } +} diff --git a/src/SPC/builder/windows/library/WindowsLibraryBase.php b/src/SPC/builder/windows/library/WindowsLibraryBase.php new file mode 100644 index 00000000..c3fa49d6 --- /dev/null +++ b/src/SPC/builder/windows/library/WindowsLibraryBase.php @@ -0,0 +1,40 @@ +builder; + } + + /** + * Create a nmake wrapper file. + * + * @param string $content nmake wrapper content + * @param string $default_filename default nmake wrapper filename + * @throws FileSystemException + */ + public function makeNmakeWrapper(string $content, string $default_filename = ''): string + { + if ($default_filename === '') { + $default_filename = $this->source_dir . '\nmake_wrapper.bat'; + } + FileSystem::writeFile($default_filename, $content); + return 'nmake_wrapper.bat'; + } +} diff --git a/src/SPC/builder/windows/library/openssl.php b/src/SPC/builder/windows/library/openssl.php new file mode 100644 index 00000000..18b98438 --- /dev/null +++ b/src/SPC/builder/windows/library/openssl.php @@ -0,0 +1,44 @@ +cd($this->source_dir) + ->execWithWrapper( + $this->builder->makeSimpleWrapper($perl), + 'Configure zlib VC-WIN64A ' . + 'no-shared ' . + '--prefix=' . quote(BUILD_ROOT_PATH) . ' ' . + '--with-zlib-lib=' . quote(BUILD_LIB_PATH) . ' ' . + '--with-zlib-include=' . quote(BUILD_INCLUDE_PATH) . ' ' . + '--release ' . + 'no-legacy ' + ); + + // patch zlib + FileSystem::replaceFileStr($this->source_dir . '\Makefile', 'ZLIB1', 'zlibstatic.lib'); + // patch debug: https://stackoverflow.com/questions/18486243/how-do-i-build-openssl-statically-linked-against-windows-runtime + FileSystem::replaceFileStr($this->source_dir . '\Makefile', '/debug', '/incremental:no /opt:icf /dynamicbase /nxcompat /ltcg /nodefaultlib:msvcrt'); + cmd()->cd($this->source_dir)->execWithWrapper( + $this->builder->makeSimpleWrapper('nmake'), + 'install_dev ' . + 'CNF_LDFLAGS="/NODEFAULTLIB:kernel32.lib /NODEFAULTLIB:msvcrt /NODEFAULTLIB:msvcrtd /DEFAULTLIB:libcmt /LIBPATH:' . BUILD_LIB_PATH . ' zlibstatic.lib"' + ); + copy($this->source_dir . '\ms\applink.c', BUILD_INCLUDE_PATH . '\openssl\applink.c'); + } +} diff --git a/src/SPC/builder/windows/library/zlib.php b/src/SPC/builder/windows/library/zlib.php new file mode 100644 index 00000000..03fd033b --- /dev/null +++ b/src/SPC/builder/windows/library/zlib.php @@ -0,0 +1,38 @@ +source_dir . '\build'); + + // start build + cmd()->cd($this->source_dir) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + '-B build ' . + '-A x64 ' . + "-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " . + '-DCMAKE_BUILD_TYPE=Release ' . + '-DBUILD_SHARED_LIBS=OFF ' . + '-DSKIP_INSTALL_FILES=ON ' . + '-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' + ) + ->execWithWrapper( + $this->builder->makeSimpleWrapper('cmake'), + "--build build --config Release --target install -j{$this->builder->concurrency}" + ); + copy(BUILD_LIB_PATH . '\zlibstatic.lib', BUILD_LIB_PATH . '\zlib_a.lib'); + unlink(BUILD_ROOT_PATH . '\bin\zlib.dll'); + unlink(BUILD_LIB_PATH . '\zlib.lib'); + } +} diff --git a/src/SPC/command/BuildCliCommand.php b/src/SPC/command/BuildCliCommand.php index 4913751c..e79bc46b 100644 --- a/src/SPC/command/BuildCliCommand.php +++ b/src/SPC/command/BuildCliCommand.php @@ -36,6 +36,7 @@ class BuildCliCommand extends BuildCommand $this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs'); $this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts'); $this->addOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside'); + $this->addOption('with-micro-ext-test', null, null, 'Enable phpmicro with extension test code'); } public function handle(): int @@ -120,13 +121,17 @@ class BuildCliCommand extends BuildCommand $fixed = ' (host system)'; } if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { - logger()->info('Static php binary path' . $fixed . ': ' . $build_root_path . '/bin/php'); + $win_suffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : ''; + $path = FileSystem::convertPath("{$build_root_path}/bin/php{$win_suffix}"); + logger()->info("Static php binary path{$fixed}: {$path}"); } if (($rule & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { - logger()->info('phpmicro binary path' . $fixed . ': ' . $build_root_path . '/bin/micro.sfx'); + $path = FileSystem::convertPath("{$build_root_path}/bin/micro.sfx"); + logger()->info("phpmicro binary path{$fixed}: {$path}"); } - if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) { - logger()->info('Static php-fpm binary path' . $fixed . ': ' . $build_root_path . '/bin/php-fpm'); + if (($rule & BUILD_TARGET_FPM) === BUILD_TARGET_FPM && PHP_OS_FAMILY !== 'Windows') { + $path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm"); + logger()->info("Static php-fpm binary path{$fixed}: {$path}"); } // export metadata @@ -135,7 +140,8 @@ class BuildCliCommand extends BuildCommand // export licenses $dumper = new LicenseDumper(); $dumper->addExts($extensions)->addLibs($libraries)->addSources(['php-src'])->dump(BUILD_ROOT_PATH . '/license'); - logger()->info('License path' . $fixed . ': ' . $build_root_path . '/license/'); + $path = FileSystem::convertPath("{$build_root_path}/license/"); + logger()->info("License path{$fixed}: {$path}"); return static::SUCCESS; } catch (WrongUsageException $e) { // WrongUsageException is not an exception, it's a user error, so we just print the error message diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php index 5bb6e79d..7ee74d77 100644 --- a/src/SPC/command/DownloadCommand.php +++ b/src/SPC/command/DownloadCommand.php @@ -116,6 +116,7 @@ class DownloadCommand extends BaseCommand // get source list that will be downloaded $sources = array_map('trim', array_filter(explode(',', $this->getArgument('sources')))); if (empty($sources)) { + logger()->warning('Downloading with --all option will take more times to download, we recommend you to download with --for-extensions option !'); $sources = array_keys(Config::getSources()); } } diff --git a/src/SPC/command/MicroCombineCommand.php b/src/SPC/command/MicroCombineCommand.php index 1b14979a..3987ca78 100644 --- a/src/SPC/command/MicroCombineCommand.php +++ b/src/SPC/command/MicroCombineCommand.php @@ -87,6 +87,10 @@ class MicroCombineCommand extends BaseCommand // 8. Combine ! $output = FileSystem::isRelativePath($output) ? (WORKING_DIR . '/' . $output) : $output; $file_target = file_get_contents($micro_file) . $ini_part . file_get_contents($file); + if (PHP_OS_FAMILY === 'Windows' && !str_ends_with(strtolower($output), '.exe')) { + $output .= '.exe'; + } + $output = FileSystem::convertPath($output); $result = file_put_contents($output, $file_target); if ($result === false) { $this->output->writeln('Combine failed.'); diff --git a/src/SPC/doctor/item/WindowsToolCheckList.php b/src/SPC/doctor/item/WindowsToolCheckList.php index 73092233..0115c3da 100644 --- a/src/SPC/doctor/item/WindowsToolCheckList.php +++ b/src/SPC/doctor/item/WindowsToolCheckList.php @@ -9,10 +9,22 @@ use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; use SPC\doctor\CheckResult; use SPC\exception\RuntimeException; +use SPC\store\Downloader; +use SPC\store\FileSystem; class WindowsToolCheckList { - #[AsCheckItem('if git are installed', limit_os: 'Windows', level: 999)] + #[AsCheckItem('if Visual Studio are installed', limit_os: 'Windows', level: 999)] + public function checkVS(): ?CheckResult + { + $vs_ver = SystemUtil::findVisualStudio(); + if ($vs_ver === false) { + return CheckResult::fail('Visual Studio not installed, please install VS 2022/2019.'); + } + return CheckResult::ok($vs_ver['version'] . ' ' . $vs_ver['dir']); + } + + #[AsCheckItem('if git are installed', limit_os: 'Windows', level: 998)] public function checkGit(): ?CheckResult { if (SystemUtil::findCommand('git.exe') === null) { @@ -22,7 +34,7 @@ class WindowsToolCheckList return CheckResult::ok(); } - #[AsCheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 998)] + #[AsCheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 997)] public function checkSDK(): ?CheckResult { if (!file_exists(PHP_SDK_PATH . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) { @@ -31,14 +43,80 @@ class WindowsToolCheckList return CheckResult::ok(PHP_SDK_PATH); } + #[AsCheckItem('if git associated command exists', limit_os: 'Windows', level: 996)] + public function checkGitPatch(): ?CheckResult + { + if (($path = SystemUtil::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(); + } + + #[AsCheckItem('if nasm installed', limit_os: 'Windows', level: 995)] + public function checkNasm(): ?CheckResult + { + if (SystemUtil::findCommand('nasm.exe', include_sdk_bin: true) === null) { + return CheckResult::fail('nasm.exe not found in path.', 'install-nasm'); + } + return CheckResult::ok(); + } + + #[AsCheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)] + public function checkPerl(): ?CheckResult + { + if (file_exists(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe')) { + return CheckResult::ok(BUILD_ROOT_PATH . '\perl\perl\bin\perl.exe'); + } + if (($path = SystemUtil::findCommand('perl.exe')) === null) { + return CheckResult::fail('perl not found in path.', 'install-perl'); + } + if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v')[1]), 'MSWin32')) { + return CheckResult::fail($path . ' is not built for msvc.', 'install-perl'); + } + return CheckResult::ok(); + } + #[AsFixItem('install-php-sdk')] public function installPhpSdk(): bool { try { - cmd(true)->exec('git clone https://github.com/php/php-sdk-binary-tools.git ' . PHP_SDK_PATH); + FileSystem::removeDir(PHP_SDK_PATH); + cmd(true)->exec('git.exe clone --depth 1 https://github.com/php/php-sdk-binary-tools.git ' . PHP_SDK_PATH); } catch (RuntimeException) { return false; } return true; } + + #[AsFixItem('install-nasm')] + public function installNasm(): bool + { + // The hardcoded version here is to be consistent with the version compiled by `musl-cross-toolchain`. + $nasm_ver = '2.16.01'; + $nasm_dist = "nasm-{$nasm_ver}"; + $source = [ + 'type' => 'url', + 'url' => "https://www.nasm.us/pub/nasm/releasebuilds/{$nasm_ver}/win64/{$nasm_dist}-win64.zip", + ]; + logger()->info('Downloading ' . $source['url']); + Downloader::downloadSource('nasm', $source); + FileSystem::extractSource('nasm', DOWNLOAD_PATH . "\\{$nasm_dist}-win64.zip"); + copy(SOURCE_PATH . "\\nasm\\{$nasm_dist}\\nasm.exe", PHP_SDK_PATH . '\bin\nasm.exe'); + copy(SOURCE_PATH . "\\nasm\\{$nasm_dist}\\ndisasm.exe", PHP_SDK_PATH . '\bin\ndisasm.exe'); + return true; + } + + #[AsFixItem('install-perl')] + public function installPerl(): bool + { + $url = 'https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip'; + $source = [ + 'type' => 'url', + 'url' => $url, + ]; + logger()->info("Downloading {$url}"); + Downloader::downloadSource('strawberry-perl', $source); + FileSystem::extractSource('strawberry-perl', DOWNLOAD_PATH . '\strawberry-perl-5.38.0.1-64bit-portable.zip', '../buildroot/perl'); + return true; + } } diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index affeac88..2cb0fa37 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -168,14 +168,15 @@ class Downloader public static function downloadFile(string $name, string $url, string $filename, ?string $move_path = null): void { logger()->debug("Downloading {$url}"); - pcntl_signal(SIGINT, function () use ($filename) { - if (file_exists(DOWNLOAD_PATH . '/' . $filename)) { + $cancel_func = function () use ($filename) { + if (file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename))) { logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); + unlink(FileSystem::convertPath(DOWNLOAD_PATH . '/' . $filename)); } - }); - self::curlDown(url: $url, path: DOWNLOAD_PATH . "/{$filename}"); - pcntl_signal(SIGINT, SIG_IGN); + }; + self::registerCancelEvent($cancel_func); + self::curlDown(url: $url, path: FileSystem::convertPath(DOWNLOAD_PATH . "/{$filename}")); + self::unregisterCancelEvent(); logger()->debug("Locking {$filename}"); self::lockSource($name, ['source_type' => 'archive', 'filename' => $filename, 'move_path' => $move_path]); } @@ -187,7 +188,7 @@ class Downloader */ public static function lockSource(string $name, array $data): void { - if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) { + if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.lock.json'))) { $lock = []; } else { $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? []; @@ -204,24 +205,25 @@ class Downloader */ public static function downloadGit(string $name, string $url, string $branch, ?string $move_path = null): void { - $download_path = DOWNLOAD_PATH . "/{$name}"; + $download_path = FileSystem::convertPath(DOWNLOAD_PATH . "/{$name}"); if (file_exists($download_path)) { FileSystem::removeDir($download_path); } logger()->debug("cloning {$name} source"); $check = !defined('DEBUG_MODE') ? ' -q' : ''; - pcntl_signal(SIGINT, function () use ($download_path) { + $cancel_func = function () use ($download_path) { if (is_dir($download_path)) { logger()->warning('Removing path ' . $download_path); FileSystem::removeDir($download_path); } - }); + }; + self::registerCancelEvent($cancel_func); f_passthru( - 'git clone' . $check . + SPC_GIT_EXEC . ' clone' . $check . ' --config core.autocrlf=false ' . "--branch \"{$branch}\" " . (defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '') . " --recursive \"{$url}\" \"{$download_path}\"" ); - pcntl_signal(SIGINT, SIG_IGN); + self::unregisterCancelEvent(); // Lock logger()->debug("Locking git source {$name}"); @@ -251,7 +253,6 @@ class Downloader * @param null|array $source source meta info: [type, path, rev, url, filename, regex, license] * @throws DownloaderException * @throws FileSystemException - * @throws RuntimeException */ public static function downloadSource(string $name, ?array $source = null, bool $force = false): void { @@ -359,12 +360,12 @@ class Downloader }; $headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); - $cmd = "curl -sfSL {$methodArg} {$headerArg} \"{$url}\""; + $cmd = SPC_CURL_EXEC . " -sfSL {$methodArg} {$headerArg} \"{$url}\""; if (getenv('CACHE_API_EXEC') === 'yes') { - if (!file_exists(DOWNLOAD_PATH . '/.curl_exec_cache')) { + if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'))) { $cache = []; } else { - $cache = json_decode(file_get_contents(DOWNLOAD_PATH . '/.curl_exec_cache'), true); + $cache = json_decode(file_get_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache')), true); } if (isset($cache[$cmd]) && $cache[$cmd]['expire'] >= time()) { return $cache[$cmd]['cache']; @@ -375,7 +376,7 @@ class Downloader } $cache[$cmd]['cache'] = implode("\n", $output); $cache[$cmd]['expire'] = time() + 3600; - file_put_contents(DOWNLOAD_PATH . '/.curl_exec_cache', json_encode($cache)); + file_put_contents(FileSystem::convertPath(DOWNLOAD_PATH . '/.curl_exec_cache'), json_encode($cache)); return $cache[$cmd]['cache']; } f_exec($cmd, $output, $ret); @@ -404,7 +405,35 @@ class Downloader }; $headerArg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $check = !defined('DEBUG_MODE') ? 's' : '#'; - $cmd = "curl -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\""; + $cmd = SPC_CURL_EXEC . " -{$check}fSL -o \"{$path}\" {$methodArg} {$headerArg} \"{$url}\""; f_passthru($cmd); } + + /** + * Register CTRL+C event for different OS. + * + * @param callable $callback callback function + */ + private static function registerCancelEvent(callable $callback): void + { + if (PHP_OS_FAMILY === 'Windows') { + sapi_windows_set_ctrl_handler($callback); + } elseif (extension_loaded('pcntl')) { + pcntl_signal(SIGINT, $callback); + } else { + logger()->debug('You have not enabled `pcntl` extension, cannot prevent download file corruption when Ctrl+C'); + } + } + + /** + * Unegister CTRL+C event for different OS. + */ + private static function unregisterCancelEvent(): void + { + if (PHP_OS_FAMILY === 'Windows') { + sapi_windows_set_ctrl_handler(null); + } elseif (extension_loaded('pcntl')) { + pcntl_signal(SIGINT, SIG_IGN); + } + } } diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index c08a09bb..f786c8ef 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -160,78 +160,35 @@ class FileSystem } logger()->info("extracting {$name} source to " . ($move_path ?? SOURCE_PATH . "/{$name}") . ' ...'); try { - $target = $move_path ?? (SOURCE_PATH . "/{$name}"); + $target = self::convertPath($move_path ?? (SOURCE_PATH . "/{$name}")); // Git source, just move - if (is_dir($filename)) { - self::copyDir($filename, $target); + if (is_dir(self::convertPath($filename))) { + self::copyDir(self::convertPath($filename), $target); self::emitSourceExtractHook($name); return; } + if (f_mkdir(directory: $target, recursive: true) !== true) { + throw new FileSystemException('create ' . $name . 'source dir failed'); + } + if (in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'BSD'])) { - if (f_mkdir(directory: $target, recursive: true) !== true) { - throw new FileSystemException('create ' . $name . 'source dir failed'); - } - switch (self::extname($filename)) { - case 'xz': - case 'txz': - f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"); - // f_passthru("cat {$filename} | xz -d | tar -x -C " . SOURCE_PATH . "/{$name} --strip-components 1"); - break; - case 'gz': - case 'tgz': - f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"); - break; - case 'bz2': - f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"); - break; - case 'zip': - f_passthru("unzip {$filename} -d {$target}"); - break; - // case 'zstd': - // case 'zst': - // passthru('cat ' . $filename . ' | zstd -d | tar -x -C ".SOURCE_PATH . "/' . $name . ' --strip-components 1', $ret); - // break; - case 'tar': - f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"); - break; - default: - throw new FileSystemException('unknown archive format: ' . $filename); - } + match (self::extname($filename)) { + 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), + 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), + 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), + 'zip' => f_passthru("unzip {$filename} -d {$target}"), + default => throw new FileSystemException('unknown archive format: ' . $filename), + }; } elseif (PHP_OS_FAMILY === 'Windows') { - // find 7z - $_7zExe = self::findCommandPath('7z', [ - 'C:\Program Files\7-Zip-Zstandard', - 'C:\Program Files (x86)\7-Zip-Zstandard', - 'C:\Program Files\7-Zip', - 'C:\Program Files (x86)\7-Zip', - ]); - if (!$_7zExe) { - throw new FileSystemException('windows needs 7z to unpack'); - } + // use php-sdk-binary-tools/bin/7za.exe + $_7z = self::convertPath(PHP_SDK_PATH . '/bin/7za.exe'); f_mkdir(SOURCE_PATH . "/{$name}", recursive: true); - switch (self::extname($filename)) { - case 'zstd': - case 'zst': - if (!str_contains($_7zExe, 'Zstandard')) { - throw new FileSystemException("zstd is not supported: {$filename}"); - } - // no break - case 'xz': - case 'txz': - case 'gz': - case 'tgz': - case 'bz2': - f_passthru("\"{$_7zExe}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1"); - break; - case 'tar': - f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"); - break; - case 'zip': - f_passthru("\"{$_7zExe}\" x {$filename} -o{$target}"); - break; - default: - throw new FileSystemException("unknown archive format: {$filename}"); - } + match (self::extname($filename)) { + 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), + 'xz', 'txz', 'gz', 'tgz', 'bz2' => f_passthru("\"{$_7z}\" x -so {$filename} | tar -f - -x -C {$target} --strip-components 1"), + 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), + default => throw new FileSystemException("unknown archive format: {$filename}"), + }; } self::emitSourceExtractHook($name); } catch (RuntimeException $e) { @@ -398,7 +355,7 @@ class FileSystem public static function createDir(string $path): void { if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) { - throw new FileSystemException(sprintf('无法建立目录:%s', $path)); + throw new FileSystemException(sprintf('Unable to create dir: %s', $path)); } } @@ -408,7 +365,7 @@ class FileSystem */ public static function writeFile(string $path, mixed $content, ...$args): bool|int|string { - $dir = pathinfo($path, PATHINFO_DIRNAME); + $dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME); if (!is_dir($dir) && !mkdir($dir, 0755, true)) { throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir); } diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index fc3e69d4..920d0564 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -6,6 +6,7 @@ namespace SPC\store; use SPC\builder\BuilderBase; use SPC\builder\linux\LinuxBuilder; +use SPC\builder\unix\UnixBuilderBase; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; @@ -31,6 +32,20 @@ class SourcePatcher logger()->info('Extension [' . $ext->getName() . '] patched before buildconf'); } } + // patch windows php 8.1 bug + if (PHP_OS_FAMILY === 'Windows' && $builder->getPHPVersionID() >= 80100 && $builder->getPHPVersionID() < 80200) { + logger()->info('Patching PHP 8.1 windows Fiber bug'); + FileSystem::replaceFileStr( + SOURCE_PATH . '\php-src\win32\build\config.w32', + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + "ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');" + ); + FileSystem::replaceFileStr( + SOURCE_PATH . '\php-src\win32\build\config.w32', + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + '' + ); + } } /** @@ -181,9 +196,10 @@ class SourcePatcher FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_STRLCPY 1$/m', ''); FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_STRLCAT 1$/m', ''); } - FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_OPENPTY 1$/m', ''); - - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', ''); + if ($builder instanceof UnixBuilderBase) { + FileSystem::replaceFileRegex(SOURCE_PATH . '/php-src/main/php_config.h', '/^#define HAVE_OPENPTY 1$/m', ''); + FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', ''); + } // call extension patch before make foreach ($builder->getExts() as $ext) { @@ -252,4 +268,32 @@ class SourcePatcher @unlink($embed_c_bak); return $result; } + + /** + * Patch cli SAPI Makefile for Windows. + * + * @throws FileSystemException + * @throws RuntimeException + */ + public static function patchWindowsCLITarget(): void + { + // search Makefile code line contains "$(BUILD_DIR)\php.exe:" + $content = FileSystem::readFile(SOURCE_PATH . '/php-src/Makefile'); + $lines = explode("\r\n", $content); + $line_num = 0; + $found = false; + foreach ($lines as $v) { + if (strpos($v, '$(BUILD_DIR)\php.exe:') !== false) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new RuntimeException('Cannot patch windows CLI Makefile!'); + } + $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; + $lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines)); + } } diff --git a/src/SPC/util/WindowsCmd.php b/src/SPC/util/WindowsCmd.php index cf461725..16530c59 100644 --- a/src/SPC/util/WindowsCmd.php +++ b/src/SPC/util/WindowsCmd.php @@ -50,6 +50,11 @@ class WindowsCmd 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) { diff --git a/src/globals/defines.php b/src/globals/defines.php index 26d2f6d7..aebb7f86 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use SPC\store\FileSystem; use ZM\Logger\ConsoleLogger; define('WORKING_DIR', getcwd()); @@ -10,12 +11,12 @@ define('ROOT_DIR', dirname(__DIR__, 2)); // CLI start time define('START_TIME', microtime(true)); -define('BUILD_ROOT_PATH', is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot')); -define('SOURCE_PATH', is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source')); -define('DOWNLOAD_PATH', is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads')); -define('BUILD_BIN_PATH', is_string($a = getenv('INSTALL_BIN_PATH')) ? $a : (BUILD_ROOT_PATH . '/bin')); -define('BUILD_LIB_PATH', is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib')); -define('BUILD_INCLUDE_PATH', is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include')); +define('BUILD_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'))); +define('SOURCE_PATH', FileSystem::convertPath(is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'))); +define('DOWNLOAD_PATH', FileSystem::convertPath(is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'))); +define('BUILD_BIN_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_BIN_PATH')) ? $a : (BUILD_ROOT_PATH . '/bin'))); +define('BUILD_LIB_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_LIB_PATH')) ? $a : (BUILD_ROOT_PATH . '/lib'))); +define('BUILD_INCLUDE_PATH', FileSystem::convertPath(is_string($a = getenv('INSTALL_INCLUDE_PATH')) ? $a : (BUILD_ROOT_PATH . '/include'))); define('SEPARATED_PATH', [ '/' . pathinfo(BUILD_LIB_PATH)['basename'], // lib '/' . pathinfo(BUILD_INCLUDE_PATH)['basename'], // include @@ -26,6 +27,10 @@ if (PHP_OS_FAMILY === 'Windows') { define('PHP_SDK_PATH', is_string($a = getenv('PHP_SDK_PATH')) ? $a : (WORKING_DIR . DIRECTORY_SEPARATOR . 'php-sdk-binary-tools')); } +// for windows, prevent calling Invoke-WebRequest and wsl command +const SPC_CURL_EXEC = PHP_OS_FAMILY === 'Windows' ? 'curl.exe' : 'curl'; +const SPC_GIT_EXEC = PHP_OS_FAMILY === 'Windows' ? 'git.exe' : 'git'; + // dangerous command const DANGER_CMD = [ 'rm', diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 93156f5f..e353d5b7 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -1,5 +1,7 @@ '', + 'Windows' => 'mbstring,openssl', +}; // If you want to test lib-suggests feature with extension, add them below (comma separated, example `libwebp,libavif`). -$with_libs = ''; +$with_libs = match (PHP_OS_FAMILY) { + 'Linux', 'Darwin' => '', + 'Windows' => '', +}; // Please change your test base combination. We recommend testing with `common`. // You can use `common`, `bulk`, `minimal` or `none`. -$base_combination = 'minimal'; +// note: combination is only available for *nix platform. Windows must use `none` combination +$base_combination = match (PHP_OS_FAMILY) { + 'Linux', 'Darwin' => 'minimal', + 'Windows' => 'none', +}; // -------------------------- code area, do not modify -------------------------- @@ -51,6 +63,6 @@ $final_libs = trim($with_libs, $trim_value); echo match ($argv[1]) { 'extensions' => $final_extensions, 'libs' => $final_libs, - 'cmd' => $final_extensions . ($final_libs === '' ? '' : (' --with-libs=' . $final_libs)), + 'cmd' => '"' . $final_extensions . '"' . ($final_libs === '' ? '' : (' --with-libs="' . $final_libs . '"')), default => '', }; diff --git a/src/globals/tests/openssl.php b/src/globals/tests/openssl.php new file mode 100644 index 00000000..28e586f9 --- /dev/null +++ b/src/globals/tests/openssl.php @@ -0,0 +1,6 @@ +assertEquals('he11o', file_get_contents($file)); + + unlink($file); + } + + public function testFindCommandPath() + { + $this->assertNull(FileSystem::findCommandPath('randomtestxxxxx')); + if (PHP_OS_FAMILY === 'Windows') { + $this->assertIsString(FileSystem::findCommandPath('explorer')); + } elseif (in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'FreeBSD'])) { + $this->assertIsString(FileSystem::findCommandPath('uname')); + } + } + + /** + * @throws FileSystemException + */ + public function testReadFile() + { + $file = WORKING_DIR . '/.testread'; + file_put_contents($file, 'haha'); + $content = FileSystem::readFile($file); + $this->assertEquals('haha', $content); + @unlink($file); + } + + /** + * @throws FileSystemException + */ + public function testReplaceFileUser() + { + $file = WORKING_DIR . '/.txt1'; + file_put_contents($file, 'hello'); + + FileSystem::replaceFileUser($file, function ($file) { + return str_replace('el', '55', $file); + }); + $this->assertEquals('h55lo', file_get_contents($file)); + + unlink($file); + } + + public function testExtname() + { + $this->assertEquals('exe', FileSystem::extname('/tmp/asd.exe')); + $this->assertEquals('', FileSystem::extname('/tmp/asd.')); + } + + /** + * @throws \ReflectionException + * @throws FileSystemException + */ + public function testGetClassesPsr4() + { + $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\\builder\\extension'); + foreach ($classes as $class) { + $this->assertIsString($class); + new \ReflectionClass($class); + } + } + + public function testConvertPath() + { + $this->assertEquals('phar://C:/pharfile.phar', FileSystem::convertPath('phar://C:/pharfile.phar')); + if (DIRECTORY_SEPARATOR === '\\') { + $this->assertEquals('C:\Windows\win.ini', FileSystem::convertPath('C:\Windows/win.ini')); + } + } + + /** + * @throws FileSystemException + */ + public function testCreateDir() + { + FileSystem::createDir(WORKING_DIR . '/.testdir'); + $this->assertDirectoryExists(WORKING_DIR . '/.testdir'); + rmdir(WORKING_DIR . '/.testdir'); + } + + /** + * @throws FileSystemException + */ + public function testReplaceFileStr() + { + $file = WORKING_DIR . '/.txt1'; + file_put_contents($file, 'hello'); + + FileSystem::replaceFileStr($file, 'el', '55'); + $this->assertEquals('h55lo', file_get_contents($file)); + + unlink($file); + } + + /** + * @throws FileSystemException + */ + public function testResetDir() + { + // prepare fake git dir to test + FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); + FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); + FileSystem::resetDir(WORKING_DIR . '/.fake_down_test'); + $this->assertFileDoesNotExist(WORKING_DIR . '/.fake_down_test/a.c'); + FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); + } + + /** + * @throws FileSystemException + * @throws RuntimeException + */ + public function testCopyDir() + { + // prepare fake git dir to test + FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); + FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); + FileSystem::copyDir(WORKING_DIR . '/.fake_down_test', WORKING_DIR . '/.fake_down_test2'); + $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test2'); + $this->assertFileExists(WORKING_DIR . '/.fake_down_test2/a.c'); + FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); + FileSystem::removeDir(WORKING_DIR . '/.fake_down_test2'); + } + + /** + * @throws FileSystemException + */ + public function testRemoveDir() + { + FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); + $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test'); + FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); + $this->assertDirectoryDoesNotExist(WORKING_DIR . '/.fake_down_test'); + } + + /** + * @throws FileSystemException + */ + public function testLoadConfigArray() + { + $arr = FileSystem::loadConfigArray('lib'); + $this->assertArrayHasKey('zlib', $arr); + } + + public function testIsRelativePath() + { + $this->assertTrue(FileSystem::isRelativePath('.')); + $this->assertTrue(FileSystem::isRelativePath('.\\sdf')); + if (DIRECTORY_SEPARATOR === '\\') { + $this->assertFalse(FileSystem::isRelativePath('C:\\asdasd/fwe\asd')); + } + $this->assertTrue(FileSystem::isRelativePath('/fwefwefewf')); + } + + public function testScanDirFiles() + { + $this->assertFalse(FileSystem::scanDirFiles('wfwefewfewf')); + $files = FileSystem::scanDirFiles(ROOT_DIR . '/config', true, true); + $this->assertContains('lib.json', $files); + } + + /** + * @throws FileSystemException + */ + public function testWriteFile() + { + FileSystem::writeFile(WORKING_DIR . '/.txt', 'txt'); + $this->assertFileExists(WORKING_DIR . '/.txt'); + $this->assertEquals('txt', FileSystem::readFile(WORKING_DIR . '/.txt')); + unlink(WORKING_DIR . '/.txt'); + } +}