Find this useful? Enter your email to receive occasional updates for securing PHP code.
Signing you up...
Thank you for signing up!
PHP Decode
<?php /** * This file is part of the Zephir. * * (c) Phalcon Team <[email protected]..
Decoded Output download
<?php
/**
* This file is part of the Zephir.
*
* (c) Phalcon Team <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Zephir;
use DirectoryIterator;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use ReflectionException;
use Zephir\Backend\Backend;
use Zephir\Backend\FcallManagerInterface;
use Zephir\Backend\StringsManager;
use Zephir\Class\Definition\Definition;
use Zephir\Code\ArgInfoDefinition;
use Zephir\Code\Builder\Struct;
use Zephir\Code\Printer;
use Zephir\Compiler\CompilerFileFactory;
use Zephir\Compiler\FileInterface;
use Zephir\Exception\CompilerException;
use Zephir\Exception\IllegalStateException;
use Zephir\Exception\InvalidArgumentException;
use Zephir\Exception\NotImplementedException;
use Zephir\Exception\ParseException;
use Zephir\Exception\RuntimeException;
use Zephir\FileSystem\FileSystemInterface;
use Zephir\FileSystem\HardDisk;
use Zephir\Parser\Manager;
use function array_filter;
use function array_map;
use function array_merge;
use function array_unique;
use function asort;
use function basename;
use function call_user_func;
use function chmod;
use function class_exists;
use function count;
use function defined;
use function dirname;
use function exec;
use function explode;
use function extension_loaded;
use function file;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function fwrite;
use function getcwd;
use function getenv;
use function htmlentities;
use function implode;
use function in_array;
use function interface_exists;
use function is_array;
use function is_dir;
use function is_readable;
use function is_string;
use function krsort;
use function md5;
use function md5_file;
use function mkdir;
use function ob_get_clean;
use function ob_start;
use function phpinfo;
use function preg_match;
use function preg_replace;
use function realpath;
use function sprintf;
use function str_replace;
use function strcasecmp;
use function strip_tags;
use function strlen;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;
use function trim;
use function ucfirst;
use function unlink;
use function version_compare;
use const DIRECTORY_SEPARATOR;
use const INFO_GENERAL;
use const PHP_EOL;
use const PHP_INT_SIZE;
use const ZEND_THREAD_SAFE;
use const SORT_STRING;
use const STDERR;
final class Compiler
{
use LoggerAwareTrait;
/**
* @var FunctionDefinition[]
*/
public array $functionDefinitions = [];
private array $anonymousFiles = [];
private array $compiledFiles = [];
private array $constants = [];
/**
* @var Definition[]
*/
private array $definitions = [];
private array $externalDependencies = [];
private array $extraFiles = [];
private FcallManagerInterface $fcallManager;
/**
* @var CompilerFile[]
*/
private array $files = [];
private array $globals = [];
/**
* @var Definition[]
*/
private static array $internalDefinitions = [];
/**
* Additional initializer code.
* Used for static property initialization.
*/
private array $internalInitializers = [];
private static bool $loadedPrototypes = false;
private ?string $optimizersPath;
private ?string $prototypesPath;
private StringsManager $stringManager;
private ?string $templatesPath;
public function __construct(
private Config $config,
public Backend $backend,
private Manager $parserManager,
private FileSystemInterface $filesystem,
private CompilerFileFactory $compilerFileFactory,
) {
$this->logger = new NullLogger();
$this->stringManager = new StringsManager();
$this->fcallManager = $this->backend->getFcallManager();
try {
$this->assertRequiredExtensionsIsPresent();
} catch (RuntimeException $e) {
fwrite(STDERR, trim($e->getMessage()) . PHP_EOL);
exit(1);
}
}
/**
* Inserts an anonymous class definition in the compiler.
*/
public function addClassDefinition(CompilerFileAnonymous $file, Definition $classDefinition): void
{
$this->definitions[$classDefinition->getCompleteName()] = $classDefinition;
$this->anonymousFiles[$classDefinition->getCompleteName()] = $file;
}
/**
* Adds an external dependency to the compiler.
*/
public function addExternalDependency(string $namespace, string $location): void
{
$this->externalDependencies[$namespace] = $location;
}
/**
* Adds a function to the function definitions.
*/
public function addFunction(FunctionDefinition $func, array $statement = []): void
{
$funcName = strtolower($func->getInternalName());
if (isset($this->functionDefinitions[$funcName])) {
throw new CompilerException(
"Function '" . $func->getCompleteName() . "' was defined more than one time",
$statement
);
}
$this->functionDefinitions[$funcName] = $func;
}
/**
* Generate a HTML API.
*
* @throws ConfigException
* @throws Exception
* @throws ReflectionException
*/
public function api(array $options = [], bool $fromGenerate = false): void
{
if (!$fromGenerate) {
$this->generate();
}
$templatesPath = $this->templatesPath ?: dirname(__DIR__) . '/templates';
$documentator = new Documentation($this->files, $this->config, $templatesPath, $options);
$documentator->setLogger($this->logger);
$this->logger->info('Generating API into ' . $documentator->getOutputDirectory());
$documentator->build();
}
public function calculateDependencies(array $files, $_dependency = null): void
{
/**
* Classes are ordered according to a dependency ranking
* Classes with higher rank, need to be initialized first
* We first build a dependency tree and then set the rank accordingly
*/
if (null === $_dependency) {
$dependencyTree = [];
foreach ($files as $file) {
if (!$file->isExternal()) {
$classDefinition = $file->getClassDefinition();
$dependencyTree[$classDefinition->getCompleteName()] = $classDefinition->getDependencies();
}
}
// Make sure the dependencies are loaded first (recursively)
foreach ($dependencyTree as $dependencies) {
foreach ($dependencies as $dependency) {
$dependency->increaseDependencyRank(0);
$this->calculateDependencies($dependencyTree, $dependency);
}
}
return;
}
$dependencyTree = $files;
if (isset($dependencyTree[$_dependency->getCompleteName()])) {
foreach ($dependencyTree[$_dependency->getCompleteName()] as $dependency) {
$dependency->increaseDependencyRank(0);
$this->calculateDependencies($dependencyTree, $dependency);
}
}
}
/**
* Check if the project must be phpized again.
*
* @return bool
*/
public function checkIfPhpized(): bool
{
return !file_exists('ext/Makefile');
}
/**
* Compiles the extension without installing it.
*
* @param bool $development
* @param int|null $jobs
*
* @throws Exception
*/
public function compile(bool $development = false, int $jobs = null): void
{
$jobs = $jobs ?: 2;
/**
* Get global namespace.
*/
$namespace = str_replace('\\', '_', $this->checkDirectory());
$extensionName = $this->config->get('extension-name');
if (empty($extensionName) || !is_string($extensionName)) {
$extensionName = $namespace;
}
$currentDir = getcwd();
if (file_exists("$currentDir/compile.log")) {
unlink("$currentDir/compile.log");
}
if (file_exists("$currentDir/compile-errors.log")) {
unlink("$currentDir/compile-errors.log");
}
if (file_exists("$currentDir/ext/modules/{$namespace}.so")) {
unlink("$currentDir/ext/modules/{$namespace}.so");
}
if (Os::isWindows()) {
// TODO(klay): Make this better. Looks like it is non standard Env. Var
exec('cd ext && %PHP_DEVPACK%\\phpize --clean', $output, $exit);
$releaseFolder = $this->getWindowsReleaseDir();
if (file_exists($releaseFolder)) {
exec('rd /s /q ' . $releaseFolder, $output, $exit);
}
$this->logger->info('Preparing for PHP compilation...');
// TODO(klay): Make this better. Looks like it is non standard Env. Var
exec('cd ext && %PHP_DEVPACK%\\phpize', $output, $exit);
/**
* fix until patch hits all supported PHP builds.
*
* @see https://github.com/php/php-src/commit/9a3af83ee2aecff25fd4922ef67c1fb4d2af6201
*/
$fixMarker = '/* zephir_phpize_fix */';
$configureFile = file_get_contents('ext\\configure.js');
$configureFix = ["var PHP_ANALYZER = 'disabled';", "var PHP_PGO = 'no';", "var PHP_PGI = 'no';"];
$hasChanged = false;
if (!str_contains($configureFile, $fixMarker)) {
$configureFile = $fixMarker . PHP_EOL . implode(PHP_EOL, $configureFix) . PHP_EOL . $configureFile;
$hasChanged = true;
}
/* fix php's broken phpize patching ... */
$marker = 'var build_dir = (dirname ? dirname : "").replace(new RegExp("^..\\\\\\\\"), "");';
$pos = strpos($configureFile, $marker);
if (false !== $pos) {
$spMarker = 'if (MODE_PHPIZE) {';
$sp = strpos($configureFile, $spMarker, $pos - 200);
if (false === $sp) {
throw new CompilerException('outofdate... phpize seems broken again');
}
$configureFile = substr($configureFile, 0, $sp) .
'if (false) {' . substr($configureFile, $sp + strlen($spMarker));
$hasChanged = true;
}
if ($hasChanged) {
file_put_contents('ext\\configure.js', $configureFile);
}
$this->logger->info('Preparing configuration file...');
exec('cd ext && configure --enable-' . $extensionName);
} else {
exec('cd ext && make clean && phpize --clean', $output, $exit);
$this->logger->info('Preparing for PHP compilation...');
exec('cd ext && phpize', $output, $exit);
$this->logger->info('Preparing configuration file...');
exec(
'cd ext && export CC="gcc" && export CFLAGS="' .
$this->getGccFlags($development) .
'" && ./configure --enable-' .
$extensionName
);
}
$currentDir = getcwd();
$this->logger->info('Compiling...');
if (Os::isWindows()) {
exec(
'cd ext && nmake 2>' . $currentDir . '\compile-errors.log 1>' .
$currentDir . '\compile.log',
$output,
$exit
);
} else {
$this->preCompileHeaders();
exec(
'cd ext && (make -s -j' . $jobs . ' 2>' . $currentDir . '/compile-errors.log 1>' .
$currentDir .
'/compile.log)',
$output,
$exit
);
}
}
/**
* Create config.m4 and config.w32 for the extension.
*
* TODO: move this to backend?
*
* @throws Exception
*/
public function createConfigFiles(string $project): bool
{
$contentM4 = $this->backend->getTemplateFileContents('config.m4');
if (empty($contentM4)) {
throw new Exception("Template config.m4 doesn't exist");
}
$contentW32 = $this->backend->getTemplateFileContents('config.w32');
if (empty($contentW32)) {
throw new Exception("Template config.w32 doesn't exist");
}
$safeProject = 'zend' === $project ? 'zend_' : $project;
$compiledFiles = array_map(fn($file) => str_replace('.c', '.zep.c', $file), $this->compiledFiles);
/**
* If export-classes is enabled all headers are copied to include/php/ext.
*/
$exportClasses = $this->config->get('export-classes', 'extra');
if ($exportClasses) {
$compiledHeaders = array_map(fn($file) => str_replace('.c', '.zep.h', $file), $this->compiledFiles);
} else {
$compiledHeaders = ['php_' . strtoupper($project) . '.h'];
}
/**
* Check extra-libs, extra-cflags, package-dependencies exists
*/
$extraLibs = (string)$this->config->get('extra-libs');
$extraCflags = (string)$this->config->get('extra-cflags');
$contentM4 = $this->generatePackageDependenciesM4($contentM4);
$buildDirs = [];
foreach ($compiledFiles as $file) {
$dir = dirname($file);
if (!in_array($dir, $buildDirs)) {
$buildDirs[] = $dir;
}
}
asort($buildDirs);
/**
* Generate config.m4.
*/
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_CAMELIZE%' => ucfirst($project),
'%FILES_COMPILED%' => implode("\n\t", $this->toUnixPaths($compiledFiles)),
'%HEADERS_COMPILED%' => implode(' ', $this->toUnixPaths($compiledHeaders)),
'%EXTRA_FILES_COMPILED%' => implode("\n\t", $this->toUnixPaths($this->extraFiles)),
'%PROJECT_EXTRA_LIBS%' => $extraLibs,
'%PROJECT_EXTRA_CFLAGS%' => $extraCflags,
'%PROJECT_BUILD_DIRS%' => implode(' ', $buildDirs),
];
foreach ($toReplace as $mark => $replace) {
$contentM4 = str_replace($mark, $replace, $contentM4);
}
HardDisk::persistByHash($contentM4, 'ext/config.m4');
/**
* Generate config.w32.
*/
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%FILES_COMPILED%' => implode(
"\r\n\t",
$this->processAddSources($compiledFiles, strtolower($project))
),
'%EXTRA_FILES_COMPILED%' => implode(
"\r\n\t",
$this->processAddSources($this->extraFiles, strtolower($project))
),
];
foreach ($toReplace as $mark => $replace) {
$contentW32 = str_replace($mark, $replace, $contentW32);
}
$needConfigure = HardDisk::persistByHash($contentW32, 'ext/config.w32');
/**
* php_ext.h.
*/
$content = $this->backend->getTemplateFileContents('php_ext.h');
if (empty($content)) {
throw new Exception("Template php_ext.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/php_ext.h');
/**
* ext.h.
*/
$content = $this->backend->getTemplateFileContents('ext.h');
if (empty($content)) {
throw new Exception("Template ext.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/ext.h');
/**
* ext_config.h.
*/
$content = $this->backend->getTemplateFileContents('ext_config.h');
if (empty($content)) {
throw new Exception("Template ext_config.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER%' => strtolower($project),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/ext_config.h');
/**
* ext_clean.
*/
$content = $this->backend->getTemplateFileContents('clean');
if (empty($content)) {
throw new Exception("Clean file doesn't exist");
}
if (HardDisk::persistByHash($content, 'ext/clean')) {
chmod('ext/clean', 0755);
}
/**
* ext_install.
*/
$content = $this->backend->getTemplateFileContents('install');
if (empty($content)) {
throw new Exception("Install file doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER%' => strtolower($project),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
if (HardDisk::persistByHash($content, 'ext/install')) {
chmod('ext/install', 0755);
}
return (bool)$needConfigure;
}
/**
* Create project.c and project.h according to the current extension.
*
* TODO: Move the part of the logic which depends on templates (backend-specific) to backend?
*
* @throws Exception
*/
public function createProjectFiles(string $project): bool
{
$needConfigure = $this->checkKernelFiles();
/**
* project.c.
*/
$content = $this->backend->getTemplateFileContents('project.c');
if (empty($content)) {
throw new Exception("Template project.c doesn't exist");
}
$includes = '';
$reqInitializers = '';
$reqDestructors = '';
$prqDestructors = '';
$modInitializers = '';
$modDestructors = '';
$glbInitializers = '';
$glbDestructors = '';
$files = array_merge($this->files, $this->anonymousFiles);
/**
* Round 1. Calculate the dependency rank
*/
$this->calculateDependencies($files);
$classEntries = [];
$classInits = [];
$interfaceEntries = [];
$interfaceInits = [];
/**
* Round 2. Generate the ZEPHIR_INIT calls according to the dependency rank
*/
/** @var FileInterface $file */
foreach ($files as $file) {
if ($file->isExternal()) {
continue;
}
$classDefinition = $file->getClassDefinition();
if ($classDefinition === null) {
continue;
}
$dependencyRank = $classDefinition->getDependencyRank();
if ('class' === $classDefinition->getType()) {
$classEntries[$dependencyRank][] = 'zend_class_entry *' . $classDefinition->getClassEntry() . ';';
$classInits[$dependencyRank][] = 'ZEPHIR_INIT('
. $classDefinition->getCNamespace()
. '_'
. $classDefinition->getName()
. ');';
} else {
$interfaceEntries[$dependencyRank][] = 'zend_class_entry *' . $classDefinition->getClassEntry() . ';';
$interfaceInits[$dependencyRank][] = 'ZEPHIR_INIT('
. $classDefinition->getCNamespace()
. '_'
. $classDefinition->getName()
. ');';
}
}
krsort($classInits);
krsort($classEntries);
krsort($interfaceInits);
krsort($interfaceEntries);
$completeInterfaceInits = [];
foreach ($interfaceInits as $rankInterfaceInits) {
asort($rankInterfaceInits, SORT_STRING);
$completeInterfaceInits = array_merge($completeInterfaceInits, $rankInterfaceInits);
}
$completeInterfaceEntries = [];
foreach ($interfaceEntries as $rankInterfaceEntries) {
asort($rankInterfaceEntries, SORT_STRING);
$completeInterfaceEntries = array_merge($completeInterfaceEntries, $rankInterfaceEntries);
}
$completeClassInits = [];
foreach ($classInits as $rankClassInits) {
asort($rankClassInits, SORT_STRING);
$completeClassInits = array_merge($completeClassInits, $rankClassInits);
}
$completeClassEntries = [];
foreach ($classEntries as $rankClassEntries) {
asort($rankClassEntries, SORT_STRING);
$completeClassEntries = array_merge($completeClassEntries, $rankClassEntries);
}
/**
* Round 3. Process extension globals
*/
[$globalCode, $globalStruct, $globalsDefault, $initEntries] = $this->processExtensionGlobals($project);
if ('zend' == $project) {
$safeProject = 'zend_';
} else {
$safeProject = $project;
}
/**
* Round 4. Process extension info.
*/
$phpInfo = $this->processExtensionInfo();
/**
* Round 5. Generate Function entries (FE)
*/
[$feHeader, $feEntries] = $this->generateFunctionInformation();
/**
* Check if there are module/request/global destructors.
*/
$destructors = $this->config->get('destructors');
if (is_array($destructors)) {
$invokeRequestDestructors = $this->processCodeInjection($destructors, 'request');
$includes .= PHP_EOL . $invokeRequestDestructors[0];
$reqDestructors = $invokeRequestDestructors[1];
$invokePostRequestDestructors = $this->processCodeInjection($destructors, 'post-request');
$includes .= PHP_EOL . $invokePostRequestDestructors[0];
$prqDestructors = $invokePostRequestDestructors[1];
$invokeModuleDestructors = $this->processCodeInjection($destructors, 'module');
$includes .= PHP_EOL . $invokeModuleDestructors[0];
$modDestructors = $invokeModuleDestructors[1];
$invokeGlobalsDestructors = $this->processCodeInjection($destructors, 'globals');
$includes .= PHP_EOL . $invokeGlobalsDestructors[0];
$glbDestructors = $invokeGlobalsDestructors[1];
}
/**
* Check if there are module/request/global initializers.
*/
$initializers = $this->config->get('initializers');
if (is_array($initializers)) {
$invokeRequestInitializers = $this->processCodeInjection($initializers, 'request');
$includes .= PHP_EOL . $invokeRequestInitializers[0];
$reqInitializers = $invokeRequestInitializers[1];
$invokeModuleInitializers = $this->processCodeInjection($initializers, 'module');
$includes .= PHP_EOL . $invokeModuleInitializers[0];
$modInitializers = $invokeModuleInitializers[1];
$invokeGlobalsInitializers = $this->processCodeInjection($initializers, 'globals');
$includes .= PHP_EOL . $invokeGlobalsInitializers[0];
$glbInitializers = $invokeGlobalsInitializers[1];
}
/**
* Append extra details.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['init'])) {
$completeClassInits[] = 'ZEPHIR_INIT(' . $value['init'] . ')';
}
if (isset($value['entry'])) {
$completeClassEntries[] = 'zend_class_entry *' . $value['entry'] . ';';
}
}
}
$modRequires = array_map(
fn($mod) => sprintf('ZEND_MOD_REQUIRED("%s")', strtolower($mod)),
$this->config->get('extensions', 'requires') ?: []
);
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_CAMELIZE%' => ucfirst($project),
'%CLASS_ENTRIES%' => implode(
PHP_EOL,
array_merge($completeInterfaceEntries, $completeClassEntries)
),
'%CLASS_INITS%' => implode(
PHP_EOL . "\t",
array_merge($completeInterfaceInits, $completeClassInits)
),
'%INIT_GLOBALS%' => implode(
PHP_EOL . "\t",
array_merge((array)$globalsDefault[0], [$glbInitializers])
),
'%INIT_MODULE_GLOBALS%' => $globalsDefault[1],
'%DESTROY_GLOBALS%' => $glbDestructors,
'%EXTENSION_INFO%' => $phpInfo,
'%EXTRA_INCLUDES%' => implode(
PHP_EOL,
array_unique(explode(PHP_EOL, $includes))
),
'%MOD_INITIALIZERS%' => $modInitializers,
'%MOD_DESTRUCTORS%' => $modDestructors,
'%REQ_INITIALIZERS%' => implode(
PHP_EOL . "\t",
array_merge($this->internalInitializers, [$reqInitializers])
),
'%REQ_DESTRUCTORS%' => $reqDestructors,
'%POSTREQ_DESTRUCTORS%' => empty($prqDestructors) ? '' : implode(
PHP_EOL,
[
'#define ZEPHIR_POST_REQUEST 1',
'static PHP_PRSHUTDOWN_FUNCTION(' . strtolower($project) . ')',
'{',
"\t" . implode(
PHP_EOL . "\t",
explode(PHP_EOL, $prqDestructors)
),
'}',
]
),
'%FE_HEADER%' => $feHeader,
'%FE_ENTRIES%' => $feEntries,
'%PROJECT_INI_ENTRIES%' => implode(PHP_EOL . "\t", $initEntries),
'%PROJECT_DEPENDENCIES%' => implode(PHP_EOL . "\t", $modRequires),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
/**
* Round 5. Generate and place the entry point of the project
*/
HardDisk::persistByHash($content, 'ext/' . $safeProject . '.c');
unset($content);
/**
* Round 6. Generate the project main header.
*/
$content = $this->backend->getTemplateFileContents('project.h');
if (empty($content)) {
throw new Exception("Template project.h doesn't exists");
}
$includeHeaders = [];
foreach ($this->compiledFiles as $file) {
if ($file) {
$fileH = str_replace('.c', '.zep.h', $file);
$include = '#include "' . $fileH . '"';
$includeHeaders[] = $include;
}
}
/**
* Append extra headers.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['header'])) {
$include = '#include "' . $value['header'] . '"';
$includeHeaders[] = $include;
}
}
}
$toReplace = [
'%INCLUDE_HEADERS%' => implode(PHP_EOL, $includeHeaders),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/' . $safeProject . '.h');
unset($content);
/**
* Round 7. Create php_project.h.
*/
$content = $this->backend->getTemplateFileContents('php_project.h');
if (empty($content)) {
throw new Exception("Template php_project.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_EXTNAME%' => strtolower($project),
'%PROJECT_NAME%' => mb_convert_encoding($this->config->get('name'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_AUTHOR%' => mb_convert_encoding($this->config->get('author'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_VERSION%' => mb_convert_encoding($this->config->get('version'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_DESCRIPTION%' => mb_convert_encoding(
$this->config->get('description'),
'ISO-8859-1',
'UTF-8'
),
'%PROJECT_ZEPVERSION%' => Zephir::VERSION,
'%EXTENSION_GLOBALS%' => $globalCode,
'%EXTENSION_STRUCT_GLOBALS%' => $globalStruct,
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/php_' . $safeProject . '.h');
unset($content);
return $needConfigure;
}
/**
* Generates the C sources from Zephir without compiling them.
*
* @throws Exception
* @throws ReflectionException
*/
public function generate(bool $fromGenerate = false): bool
{
/**
* Get global namespace.
*/
$namespace = $this->checkDirectory();
/**
* Check whether there are external dependencies.
*/
$externalDependencies = $this->config->get('external-dependencies');
if (is_array($externalDependencies)) {
foreach ($externalDependencies as $dependencyNs => $location) {
if (!file_exists($location)) {
throw new CompilerException(
sprintf(
'Location of dependency "%s" does not exist. Check the config.json for more information.',
$dependencyNs
)
);
}
$this->addExternalDependency($dependencyNs, $location);
}
}
/**
* Round 1. pre-compile all files in memory
*/
$this->recursivePreCompile(str_replace('\\', DIRECTORY_SEPARATOR, $namespace));
if (!count($this->files)) {
throw new Exception(
"Zephir files to compile couldn't be found. Did you add a first class to the extension?"
);
}
/**
* Round 2. Check 'extends' and 'implements' dependencies
*/
foreach ($this->files as $compileFile) {
$compileFile->checkDependencies($this);
}
/**
* Sort the files by dependency ranking.
*/
$files = [];
$rankedFiles = [];
$this->calculateDependencies($this->files);
foreach ($this->files as $rankFile) {
$rank = $rankFile->getClassDefinition()->getDependencyRank();
$rankedFiles[$rank][] = $rankFile;
}
krsort($rankedFiles);
foreach ($rankedFiles as $rankFiles) {
$files = array_merge($files, $rankFiles);
}
$this->files = $files;
/**
* Convert C-constants into PHP constants.
*/
$constantsSources = $this->config->get('constants-sources');
if (is_array($constantsSources)) {
$this->loadConstantsSources($constantsSources);
}
/**
* Set extension globals.
*/
$globals = $this->config->get('globals');
if (is_array($globals)) {
$this->setExtensionGlobals($globals);
}
/**
* Load function optimizers
*/
if (false === self::$loadedPrototypes) {
$optimizersPath = $this->resolveOptimizersPath();
FunctionCall::addOptimizerDir("{$optimizersPath}/FunctionCall");
$customOptimizersPaths = $this->config->get('optimizer-dirs');
if (is_array($customOptimizersPaths)) {
foreach ($customOptimizersPaths as $directory) {
FunctionCall::addOptimizerDir(realpath($directory));
}
}
/**
* Load additional extension prototypes.
*/
$prototypesPath = $this->resolvePrototypesPath();
foreach (new DirectoryIterator($prototypesPath) as $file) {
if ($file->isDir() || $file->isDot()) {
continue;
}
// Do not use $file->getRealPath() because it does not work inside phar
$realPath = "{$file->getPath()}/{$file->getFilename()}";
$extension = $file->getBasename(".{$file->getExtension()}");
if (!extension_loaded($extension)) {
require_once $realPath;
}
}
/**
* Load customer additional extension prototypes.
*/
$prototypeDirs = $this->config->get('prototype-dir');
if (is_array($prototypeDirs)) {
foreach ($prototypeDirs as $prototype => $prototypeDir) {
/**
* Check if the extension is installed
*/
if (!extension_loaded($prototype)) {
$prototypeRealpath = realpath($prototypeDir);
if ($prototypeRealpath) {
foreach (new RecursiveDirectoryIterator($prototypeRealpath) as $file) {
if ($file->isDir()) {
continue;
}
require_once $file->getRealPath();
}
}
}
}
}
self::$loadedPrototypes = true;
}
/**
* Round 3. Compile all files to C sources.
*/
$files = [];
$hash = '';
foreach ($this->files as $compileFile) {
/**
* Only compile classes in the local extension, ignore external classes
*/
if (!$compileFile->isExternal()) {
$compileFile->compile($this, $this->stringManager);
$compiledFile = $compileFile->getCompiledFile();
$methods = [];
$classDefinition = $compileFile->getClassDefinition();
foreach ($classDefinition->getMethods() as $method) {
$methods[] = '[' . $method->getName() . ':' . implode('-', $method->getVisibility()) . ']';
if ($method->isInitializer() && $method->isStatic()) {
$this->internalInitializers[] = "\t" . $method->getName() . '();';
}
}
$files[] = $compiledFile;
$hash .= '|'
. $compiledFile
. ':'
. $classDefinition->getClassEntry()
. '['
. implode('|', $methods)
. ']';
}
}
/**
* Round 3.2. Compile anonymous classes
*/
foreach ($this->anonymousFiles as $compileFile) {
$compileFile->compile($this, $this->stringManager);
$compiledFile = $compileFile->getCompiledFile();
$methods = [];
$classDefinition = $compileFile->getClassDefinition();
foreach ($classDefinition->getMethods() as $method) {
$methods[] = '['
. $method->getName()
. ':'
. implode('-', $method->getVisibility())
. ']';
}
$files[] = $compiledFile;
$hash .= '|'
. $compiledFile
. ':'
. $classDefinition->getClassEntry()
. '['
. implode('|', $methods)
. ']';
}
$hash = md5($hash);
$this->compiledFiles = $files;
/**
* Round 3.3. Load extra C-sources.
*/
$extraSources = $this->config->get('extra-sources');
if (is_array($extraSources)) {
$this->extraFiles = $extraSources;
} else {
$this->extraFiles = [];
}
/**
* Round 3.4. Load extra classes sources.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['source'])) {
$this->extraFiles[] = $value['source'];
}
}
}
/**
* Round 4. Create config.m4 and config.w32 files / Create project.c and project.h files.
*/
$namespace = str_replace('\\', '_', $namespace);
$extensionName = $this->config->get('extension-name');
if (empty($extensionName) || !is_string($extensionName)) {
$extensionName = $namespace;
}
$needConfigure = $this->createConfigFiles($extensionName);
$needConfigure |= $this->createProjectFiles($extensionName);
$needConfigure |= $this->checkIfPhpized();
// Bitwise returns `int` instead of `bool`.
$needConfigure = (bool)$needConfigure;
/**
* When a new file is added or removed we need to run configure again
*/
if (!$fromGenerate) {
if (false === $this->filesystem->exists('compiled-files-sum')) {
$needConfigure = true;
$this->filesystem->write('compiled-files-sum', $hash);
} else {
if ($this->filesystem->read('compiled-files-sum') != $hash) {
$needConfigure = true;
$this->filesystem->delete('compiled-files-sum');
$this->filesystem->write('compiled-files-sum', $hash);
}
}
}
/**
* Round 5. Generate concatenation functions
*/
$this->stringManager->genConcatCode();
$this->fcallManager->genFcallCode();
if ($this->config->get('stubs-run-after-generate', 'stubs')) {
$this->stubs($fromGenerate);
}
return $needConfigure;
}
public function generateFunctionInformation(): array
{
$headerPrinter = new Printer();
$entryPrinter = new Printer();
/**
* Specifying Argument Information
*/
foreach ($this->functionDefinitions as $func) {
$argInfo = new ArgInfoDefinition(
$func->getArgInfoName(),
$func,
$headerPrinter,
$func->getCallGathererPass()->getCompilationContext()
);
$funcName = $func->getInternalName();
$argInfoName = $func->getArgInfoName();
$headerPrinter->output('PHP_FUNCTION(' . $funcName . ');');
$argInfo->setBooleanDefinition('_IS_BOOL');
$argInfo->setRichFormat(true);
$argInfo->render();
/** Generate FE's */
$paramData = 'NULL';
$richFormat = $func->isReturnTypesHintDetermined() && $func->areReturnTypesCompatible();
if ($richFormat || $func->hasParameters()) {
$paramData = $argInfoName;
}
if ($func->isGlobal()) {
$entryPrinter->output(
'ZEND_NAMED_FE(' . $func->getName() . ', ZEND_FN(' . $funcName . '), ' . $paramData . ')'
);
} else {
$entryPrinter->output(
'ZEND_NS_NAMED_FE("' . str_replace('\\', '\\\\', $func->getNamespace()) . '", ' .
$func->getName() .
', ZEND_FN(' . $funcName . '), ' .
$paramData . ')'
);
}
}
$entryPrinter->output('ZEND_FE_END');
return [$headerPrinter->getOutput(), $entryPrinter->getOutput()];
}
/**
* Generate package-dependencies config for m4.
*
* TODO: Move the template depending part to backend?
*/
public function generatePackageDependenciesM4(string $contentM4): string
{
$packageDependencies = $this->config->get('package-dependencies');
if (!is_array($packageDependencies)) {
return str_replace('%PROJECT_PACKAGE_DEPENDENCIES%', '', $contentM4);
}
$pkgconfigM4 = $this->backend->getTemplateFileContents('pkg-config.m4');
$pkgconfigCheckM4 = $this->backend->getTemplateFileContents('pkg-config-check.m4');
$extraCFlags = '';
foreach ($packageDependencies as $pkg => $version) {
$pkgM4Buf = $pkgconfigCheckM4;
$operator = '=';
$operatorCmd = '--exact-version';
$ar = explode('=', $version);
if (1 === count($ar)) {
if ('*' === $version) {
$version = '0.0.0';
$operator = '>=';
$operatorCmd = '--atleast-version';
}
} else {
switch ($ar[0]) {
case '<':
$operator = '<=';
$operatorCmd = '--max-version';
break;
case '>':
$operator = '>=';
$operatorCmd = '--atleast-version';
break;
}
$version = trim($ar[1]);
}
$toReplace = [
'%PACKAGE_LOWER%' => strtolower($pkg),
'%PACKAGE_UPPER%' => strtoupper($pkg),
'%PACKAGE_REQUESTED_VERSION%' => $operator . ' ' . $version,
'%PACKAGE_PKG_CONFIG_COMPARE_VERSION%' => $operatorCmd . '=' . $version,
];
foreach ($toReplace as $mark => $replace) {
$pkgM4Buf = str_replace($mark, $replace, $pkgM4Buf);
}
$pkgconfigM4 .= $pkgM4Buf;
$extraCFlags .= '$PHP_' . strtoupper($pkg) . '_INCS ';
}
$contentM4 = str_replace('%PROJECT_EXTRA_CFLAGS%', '%PROJECT_EXTRA_CFLAGS% ' . $extraCFlags, $contentM4);
return str_replace('%PROJECT_PACKAGE_DEPENDENCIES%', $pkgconfigM4, $contentM4);
}
/**
* Returns class the class definition from a given class name.
*/
public function getClassDefinition(string $className): Definition | bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className)) {
return $value;
}
}
return false;
}
/**
* Returns a Zephir Constant by its name.
*/
public function getConstant(string $name): mixed
{
return $this->constants[$name];
}
/**
* Returns an extension global by its name.
*/
public function getExtensionGlobal(string $name): array
{
return $this->globals[$name];
}
/**
* Returns GCC flags for current compilation.
*/
public function getGccFlags(bool $development = false): string
{
if (Os::isWindows()) {
// TODO
return '';
}
$gccFlags = getenv('CFLAGS');
if (!is_string($gccFlags)) {
if (false === $development) {
$gccVersion = $this->getGccVersion();
if (version_compare($gccVersion, '4.6.0', '>=')) {
$gccFlags = '-O2 -fvisibility=hidden -Wparentheses -flto -DZEPHIR_RELEASE=1';
} else {
$gccFlags = '-O2 -fvisibility=hidden -Wparentheses -DZEPHIR_RELEASE=1';
}
} else {
$gccFlags = '-O0 -g3';
}
}
return $gccFlags;
}
/**
* Returns class the class definition from a given class name.
*
* @throws ReflectionException
*/
public function getInternalClassDefinition(string $className): Definition
{
if (!isset(self::$internalDefinitions[$className])) {
$reflection = new ReflectionClass($className);
self::$internalDefinitions[$className] = Definition::buildFromReflection($reflection);
}
return self::$internalDefinitions[$className];
}
/**
* Gets the Zephir Parser Manager.
*
* @deprecated
*/
public function getParserManager(): Parser\Manager
{
return $this->parserManager;
}
/**
* Returns the php include directories returned by php-config.
*/
public function getPhpIncludeDirs(): string
{
$this->filesystem->system('php-config --includes', 'stdout', 'php-includes');
return trim($this->filesystem->read('php-includes'));
}
/**
* Returns a short user path.
*/
public static function getShortUserPath(string $path): string
{
return str_replace('\\', '/', str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path));
}
/**
* Compiles and installs the extension.
*
* @throws Exception
* @throws NotImplementedException
* @throws CompilerException
*/
public function install(bool $development = false): void
{
// Get global namespace
$namespace = str_replace('\\', '_', $this->checkDirectory());
$currentDir = getcwd();
if (Os::isWindows()) {
throw new NotImplementedException('Installation is not implemented for Windows yet. Aborting.');
}
$this->logger->info('Installing...');
$gccFlags = $this->getGccFlags($development);
$command = strtr(
// TODO: Sort out with sudo
'cd ext && export CC="gcc" && export CFLAGS=":cflags" && ' .
'make 2>> ":stderr" 1>> ":stdout" && ' .
'sudo make install 2>> ":stderr" 1>> ":stdout"',
[
':cflags' => $gccFlags,
':stderr' => "{$currentDir}/compile-errors.log",
':stdout' => "{$currentDir}/compile.log",
]
);
array_map(function ($entry): void {
if (!empty($entry)) {
$this->logger->debug(trim($entry));
}
}, explode('&&', $command));
exec($command, $output, $exit);
$fileName = $this->config->get('extension-name') ?: $namespace;
if (false === file_exists("{$currentDir}/ext/modules/{$fileName}.so")) {
throw new CompilerException(
'Internal extension compilation failed. Check compile-errors.log for more information.'
);
}
}
/**
* Allows checking if a class is part of PHP.
*/
public function isBundledClass(string $className): bool
{
return class_exists($className, false);
}
/**
* Allows checking if an interface is part of PHP.
*/
public function isBundledInterface(string $className): bool
{
return interface_exists($className, false);
}
/**
* Allows to check if a class is part of the compiled extension.
*/
public function isClass(string $className): bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className) && 'class' === $value->getType()) {
return true;
}
}
/**
* Try to autoload the class from an external dependency
*/
foreach ($this->externalDependencies as $namespace => $location) {
if (preg_match('#^' . $namespace . '\\\\#i', $className)) {
return $this->loadExternalClass($className, $location);
}
}
return false;
}
/**
* Checks if $name is a Zephir constant.
*/
public function isConstant(string $name): bool
{
return isset($this->constants[$name]);
}
/**
* Checks if a specific extension global is defined.
*/
public function isExtensionGlobal(string $name): bool
{
return isset($this->globals[$name]);
}
/**
* Allows checking if an interface is part of the compiled extension.
*
* @throws CompilerException
* @throws IllegalStateException
* @throws ParseException
*/
public function isInterface(string $className): bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className) && Definition::TYPE_INTERFACE === $value->getType()) {
return true;
}
}
/**
* Try to autoload the class from an external dependency
*/
foreach ($this->externalDependencies as $namespace => $location) {
if (preg_match('#^' . $namespace . '\\\\#i', $className)) {
return $this->loadExternalClass($className, $location);
}
}
return false;
}
/**
* Loads a class definition in an external dependency.
*
* @throws CompilerException
* @throws IllegalStateException
* @throws ParseException
*/
public function loadExternalClass(string $className, string $location): bool
{
$filePath = $location
. DIRECTORY_SEPARATOR
. strtolower(
str_replace('\\', DIRECTORY_SEPARATOR, $className)
)
. '.zep';
/**
* Fix the class name.
*/
$className = implode(
'\\',
array_map(
'ucfirst',
explode('\\', $className)
)
);
if (isset($this->files[$className])) {
return true;
}
if (!file_exists($filePath)) {
return false;
}
/** @var CompilerFile|CompilerFileAnonymous $compilerFile */
$compilerFile = $this->compilerFileFactory->create($className, $filePath);
$compilerFile->setIsExternal(true);
$compilerFile->preCompile($this);
$this->files[$className] = $compilerFile;
$this->definitions[$className] = $compilerFile->getClassDefinition();
return true;
}
/**
* Pre-compile headers to speed up compilation.
*/
public function preCompileHeaders(): void
{
if (Os::isWindows()) {
// TODO: Add Windows support
return;
}
$phpIncludes = $this->getPhpIncludeDirs();
/** @var DirectoryIterator $file */
foreach (new DirectoryIterator('ext/kernel') as $file) {
if ($file->isDir() || $file->getExtension() !== 'h') {
continue;
}
$command = sprintf(
'cd ext && gcc -c kernel/%s -I. %s -o kernel/%s.gch',
$file->getBaseName(),
$phpIncludes,
$file->getBaseName()
);
$path = $file->getRealPath();
if (!file_exists($path . '.gch') || filemtime($path) > filemtime($path . '.gch')) {
$this->filesystem->system($command, 'stdout', 'compile-header');
}
}
}
/**
* Process extension code injection.
*/
public function processCodeInjection(array $entries, string $section = 'request'): array
{
$codes = [];
$includes = [];
if (isset($entries[$section])) {
foreach ($entries[$section] as $entry) {
if (!empty($entry['code'])) {
$codes[] = $entry['code'] . ';';
}
if (!empty($entry['include'])) {
$includes[] = '#include "' . $entry['include'] . '"';
}
}
}
return [implode(PHP_EOL, $includes), implode("\n\t", $codes)];
}
/**
* Process extension globals.
*
* @throws Exception
*/
public function processExtensionGlobals(string $namespace): array
{
$globalCode = '';
$globalStruct = '';
$globalsDefault = [[], []];
$initEntries = [];
/**
* Generate the extensions globals declaration.
*/
$globals = $this->config->get('globals');
if (is_array($globals)) {
$structures = [];
$variables = [];
foreach ($globals as $name => $global) {
$parts = explode('.', $name);
if (isset($parts[1])) {
$structures[$parts[0]][$parts[1]] = $global;
} else {
$variables[$parts[0]] = $global;
}
}
/**
* Process compound structures
*/
foreach ($structures as $structureName => $internalStructure) {
if (preg_match('/^[0-9a-zA-Z_]$/', $structureName)) {
throw new Exception("Struct name: '" . $structureName . "' contains invalid characters");
}
$structBuilder = new Struct('_zephir_struct_' . $structureName, $structureName);
foreach ($internalStructure as $field => $global) {
if (preg_match('/^[0-9a-zA-Z_]$/', $field)) {
throw new Exception("Struct field name: '" . $field . "' contains invalid characters");
}
$structBuilder->addProperty($field, $global['type']);
$isModuleGlobal = (int)!empty($global['module']);
$globalsDefault[$isModuleGlobal][] = $structBuilder->getCDefault($field, $global, $namespace);
$initEntries[] = $structBuilder->getInitEntry($field, $global, $namespace);
}
$globalStruct .= $structBuilder . PHP_EOL;
}
$globalCode = PHP_EOL;
foreach ($structures as $structureName => $internalStructure) {
$globalCode .= "\t" . 'zephir_struct_' . $structureName . ' ' . $structureName . ';' . PHP_EOL;
}
/**
* Process single variables
*/
foreach ($variables as $name => $global) {
if (preg_match('/^[0-9a-zA-Z_]$/', $name)) {
throw new Exception("Extension global variable name: '" . $name . "' contains invalid characters");
}
if (!isset($global['default'])) {
throw new Exception("Extension global variable name: '" . $name . "' contains invalid characters");
}
$isModuleGlobal = (int)!empty($global['module']);
$type = $global['type'];
// TODO: Add support for 'hash'
// TODO: Zephir\Optimizers\FunctionCall\GlobalsSetOptimizer
switch ($global['type']) {
case 'boolean':
case 'bool':
$type = 'zend_bool';
if (true === $global['default']) {
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = 1;';
} else {
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = 0;';
}
break;
case 'int':
case 'uint':
case 'long':
case 'double':
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = ' . $global['default'] . ';';
break;
case 'char':
case 'uchar':
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = \'' . $global['default'] . '\';';
break;
case 'string':
$type = 'char *';
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = ZSTR_VAL(zend_string_init(ZEND_STRL("' . $global['default'] . '"), 0));';
break;
default:
throw new Exception(
"Unknown type '" . $global['type'] . "' for extension global '" . $name . "'"
);
}
$globalCode .= "\t" . $type . ' ' . $name . ';' . PHP_EOL;
$iniEntry = $global['ini-entry'] ?? [];
$iniName = $iniEntry['name'] ?? $namespace . '.' . $name;
$scope = $iniEntry['scope'] ?? 'PHP_INI_ALL';
switch ($global['type']) {
case 'boolean':
case 'bool':
$initEntries[] =
'STD_PHP_INI_BOOLEAN("' .
$iniName .
'", "' .
(int)(true === $global['default']) .
'", ' .
$scope .
', OnUpdateBool, ' .
$name .
', zend_' .
$namespace .
'_globals, ' .
$namespace . '_globals)';
break;
case 'string':
$initEntries[] = sprintf(
'STD_PHP_INI_ENTRY(%s, %s, %s, NULL, %s, %s, %s)',
'"' . $iniName . '"',
'"' . $global['default'] . '"',
$scope,
$name,
'zend_' . $namespace . '_globals',
$namespace . '_globals',
);
break;
}
}
}
$globalsDefault[0] = implode(PHP_EOL, $globalsDefault[0]);
$globalsDefault[1] = implode(PHP_EOL, $globalsDefault[1]);
return [$globalCode, $globalStruct, $globalsDefault, $initEntries];
}
/**
* Generates phpinfo() sections showing information about the extension.
*/
public function processExtensionInfo(): string
{
$phpinfo = '';
$info = $this->config->get('info');
if (!is_array($info)) {
return $phpinfo;
}
foreach ($info as $table) {
$phpinfo .= "\t" . 'php_info_print_table_start();' . PHP_EOL;
if (isset($table['header'])) {
$headerArray = [];
foreach ($table['header'] as $header) {
$headerArray[] = '"' . htmlentities($header) . '"';
}
$phpinfo .= "\t" . 'php_info_print_table_header(' . count($headerArray) . ', ' .
implode(', ', $headerArray) . ');' . PHP_EOL;
}
if (isset($table['rows'])) {
foreach ($table['rows'] as $row) {
$rowArray = [];
foreach ($row as $field) {
$rowArray[] = '"' . htmlentities($field) . '"';
}
$phpinfo .= "\t" . 'php_info_print_table_row(' . count($rowArray) . ', ' .
implode(', ', $rowArray) . ');' . PHP_EOL;
}
}
$phpinfo .= "\t" . 'php_info_print_table_end();' . PHP_EOL;
}
return $phpinfo;
}
/**
* Sets extensions globals.
*/
public function setExtensionGlobals(array $globals): void
{
foreach ($globals as $key => $value) {
$this->globals[$key] = $value;
}
}
public function setOptimizersPath(string $optimizersPath): void
{
$this->optimizersPath = $optimizersPath;
}
public function setPrototypesPath(string $prototypesPath): void
{
$this->prototypesPath = $prototypesPath;
}
public function setTemplatesPath(string $templatesPath): void
{
$this->templatesPath = $templatesPath;
}
/**
* Generate IDE stubs.
*
* @throws Exception
* @throws ReflectionException
*/
public function stubs(bool $fromGenerate = false): void
{
if (!$fromGenerate) {
$this->generate();
}
$this->logger->info('Generating stubs...');
$path = str_replace(
[
'%version%',
'%namespace%',
],
[
$this->config->get('version'),
ucfirst($this->config->get('namespace')),
],
$this->config->get('path', 'stubs')
);
(new Stubs\Generator($this->files))->generate(
$this->config->get('namespace'),
$path,
$this->config->get('indent', 'extra'),
$this->config->get('banner', 'stubs') ?? ''
);
}
/**
* Ensure that required extensions is present.
*
* @throws RuntimeException
*/
private function assertRequiredExtensionsIsPresent(): void
{
$extensionRequires = $this->config->get('extensions', 'requires');
if (empty($extensionRequires)) {
return;
}
$extensions = [];
foreach ($extensionRequires as $value) {
// TODO: We'll use this as an object in the future.
if (!is_string($value)) {
continue;
}
if (!extension_loaded($value)) {
$extensions[] = $value;
}
}
if (!empty($extensions)) {
throw new RuntimeException(
sprintf(
'Could not load extension(s): %s. You must load extensions above before build this extension.',
implode(', ', $extensions)
)
);
}
}
/**
* Checks if the current directory is a valid Zephir project.
*
* @throws Exception
*/
private function checkDirectory(): string
{
$namespace = $this->config->get('namespace');
if (!$namespace) {
// TODO: Add more user friendly message.
// For example assume if the user call the command from the wrong dir
throw new Exception('Extension namespace cannot be loaded');
}
if (!is_string($namespace)) {
throw new Exception('Extension namespace is invalid');
}
if (!$this->filesystem->isInitialized()) {
$this->filesystem->initialize();
}
if (!$this->filesystem->exists('.')) {
if (!$this->checkIfPhpized()) {
$this->logger->info(
'Zephir version has changed, use "zephir fullclean" to perform a full clean of the project'
);
}
$this->filesystem->makeDirectory('.');
}
return $namespace;
}
/**
* Checks if a file must be copied.
*
* @param string $src
* @param string $dst
*
* @return bool
*/
private function checkKernelFile(string $src, string $dst): bool
{
if (preg_match('#kernels/ZendEngine[2-9]/concat\.#', $src)) {
return true;
}
if (!file_exists($dst)) {
return false;
}
return md5_file($src) === md5_file($dst);
}
/**
* Checks which files in the base kernel must be copied.
*
* @throws Exception
*/
private function checkKernelFiles(): bool
{
$kernelPath = 'ext' . DIRECTORY_SEPARATOR . 'kernel';
if (!file_exists($kernelPath)) {
if (!mkdir($kernelPath, 0775, true)) {
throw new Exception("Cannot create kernel directory: {$kernelPath}");
}
}
$kernelPath = realpath($kernelPath);
$sourceKernelPath = $this->backend->getInternalKernelPath();
$configured = $this->recursiveProcess(
$sourceKernelPath,
$kernelPath,
'@.*\.[ch]$@',
[$this, 'checkKernelFile']
);
if (!$configured) {
$this->logger->info('Cleaning old kernel files...');
$this->recursiveDeletePath($kernelPath, '@^.*\.[lcho]$@');
@mkdir($kernelPath);
$this->logger->info('Copying new kernel files...');
$this->recursiveProcess($sourceKernelPath, $kernelPath, '@^.*\.[ch]$@');
}
return !$configured;
}
/**
* Returns current GCC version.
*/
private function getGccVersion(): string
{
if (Os::isWindows()) {
return '0.0.0';
}
if ($this->filesystem->exists('gcc-version')) {
return $this->filesystem->read('gcc-version');
}
$this->filesystem->system('gcc -dumpversion', 'stdout', 'gcc-version');
$lines = $this->filesystem->file('gcc-version');
$lines = array_filter($lines);
$lastLine = $lines[count($lines) - 1];
if (preg_match('/\d+\.\d+\.\d+/', $lastLine, $matches)) {
return $matches[0];
}
return '0.0.0';
}
private function getWindowsReleaseDir(): string
{
if ($this->isZts()) {
if (PHP_INT_SIZE === 4) {
// 32-bit version of PHP
return 'ext\\Release_TS';
}
if (PHP_INT_SIZE === 8) {
// 64-bit version of PHP
return 'ext\d\\Release_TS';
}
// fallback
return 'ext\\Release_TS';
}
if (PHP_INT_SIZE === 4) {
// 32-bit version of PHP
return 'ext\\Release';
}
if (PHP_INT_SIZE === 8) {
// 64-bit version of PHP
return 'ext\d\\Release';
}
// fallback
return 'ext\\Release';
}
private function isZts(): bool
{
if (defined('ZEND_THREAD_SAFE') && ZEND_THREAD_SAFE === true) {
return true;
}
ob_start();
phpinfo(INFO_GENERAL);
return (bool)preg_match('/Thread\s*Safety\s*enabled/i', strip_tags(ob_get_clean()));
}
/**
* Registers C-constants as PHP constants from a C-file.
*
* @param array $constantsSources
*
* @throws Exception
*/
private function loadConstantsSources(array $constantsSources): void
{
foreach ($constantsSources as $constantsSource) {
if (!file_exists($constantsSource)) {
throw new Exception("File '" . $constantsSource . "' with constants definitions");
}
foreach (file($constantsSource) as $line) {
if (preg_match('/^\#define[ \t]+([A-Z0-9\_]+)[ \t]+([0-9]+)/', $line, $matches)) {
$this->constants[$matches[1]] = ['int', $matches[2]];
continue;
}
if (preg_match('/^\#define[ \t]+([A-Z0-9\_]+)[ \t]+(\'(.){1}\')/', $line, $matches)) {
$this->constants[$matches[1]] = ['char', $matches[3]];
}
}
}
}
/**
* Pre-compiles classes creating a CompilerFile definition.
*
* @throws IllegalStateException
*/
private function preCompile(string $filePath): void
{
if (!$this->parserManager->isAvailable()) {
throw new IllegalStateException($this->parserManager->requirements());
}
if (preg_match('#\.zep$#', $filePath)) {
$className = str_replace(DIRECTORY_SEPARATOR, '\\', $filePath);
$className = preg_replace('#.zep$#', '', $className);
$className = implode('\\', array_map('ucfirst', explode('\\', $className)));
$compilerFile = $this->compilerFileFactory->create($className, $filePath);
$compilerFile->preCompile($this);
$this->files[$className] = $compilerFile;
$this->definitions[$className] = $compilerFile->getClassDefinition();
}
}
/**
* Process config.w32 sections.
*
* @param array $sources
* @param string $project
*
* @return array
*/
private function processAddSources(array $sources, string $project): array
{
$groupSources = [];
foreach ($sources as $source) {
$dirName = str_replace(DIRECTORY_SEPARATOR, '/', dirname($source));
if (!isset($groupSources[$dirName])) {
$groupSources[$dirName] = [];
}
$groupSources[$dirName][] = basename($source);
}
$groups = [];
foreach ($groupSources as $dirname => $files) {
$groups[] = 'ADD_SOURCES(configure_module_dirname + "/'
. $dirname
. '", "'
. implode(' ', $files)
. '", "'
. $project
. '");';
}
return $groups;
}
/**
* Recursively deletes files in a specified location.
*
* @param string $path Directory to deletes files
* @param string $mask Regular expression to deletes files
*
* @deprecated
*
*/
private function recursiveDeletePath($path, $mask): void
{
if (!file_exists($path) || !is_dir($path) || !is_readable($path)) {
$this->logger->warning("Directory '{$path}' is not readable. Skip...");
return;
}
$objects = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($objects as $name => $object) {
if (preg_match($mask, $name)) {
@unlink($name);
}
}
}
/**
* Recursively pre-compiles all sources found in the given path.
*
* @throws IllegalStateException
* @throws InvalidArgumentException
*/
private function recursivePreCompile(string $path): void
{
if (!is_dir($path)) {
throw new InvalidArgumentException(
sprintf(
"An invalid path was passed to the compiler. Unable to obtain the '%s%s%s' directory.",
getcwd(),
DIRECTORY_SEPARATOR,
$path
)
);
}
/**
* Pre compile all files.
*/
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::SELF_FIRST
);
$files = [];
foreach ($iterator as $item) {
if (!$item->isDir()) {
$files[] = $item->getPathname();
}
}
sort($files, SORT_STRING);
foreach ($files as $file) {
$this->preCompile($file);
}
}
/**
* Copies the base kernel to the extension destination.
*
* TODO:
*
* @param $src
* @param $dest
* @param string $pattern
* @param mixed $callback
*
* @return bool
* @deprecated
*
*/
private function recursiveProcess($src, $dest, $pattern = null, $callback = 'copy')
{
$success = true;
$iterator = new DirectoryIterator($src);
foreach ($iterator as $item) {
$pathName = $item->getPathname();
if (!is_readable($pathName)) {
$this->logger->warning('File is not readable :' . $pathName);
continue;
}
$fileName = $item->getFileName();
if ($item->isDir()) {
if ('.' != $fileName && '..' != $fileName && '.libs' != $fileName) {
if (!is_dir($dest . DIRECTORY_SEPARATOR . $fileName)) {
mkdir($dest . DIRECTORY_SEPARATOR . $fileName, 0755, true);
}
$this->recursiveProcess($pathName, $dest . DIRECTORY_SEPARATOR . $fileName, $pattern, $callback);
}
} elseif (!$pattern || ($pattern && 1 === preg_match($pattern, $fileName))) {
$path = $dest . DIRECTORY_SEPARATOR . $fileName;
$success = $success && call_user_func($callback, $pathName, $path);
}
}
return $success;
}
/**
* Resolves path to the internal optimizers.
*
* @throws IllegalStateException in case of absence internal optimizers directory
*/
private function resolveOptimizersPath(): ?string
{
$optimizersPath = $this->optimizersPath;
// fallback
if (empty($optimizersPath)) {
$optimizersPath = __DIR__ . '/Optimizers';
}
if (!is_dir($optimizersPath) || !is_readable($optimizersPath)) {
throw new IllegalStateException('Unable to resolve internal optimizers directory.');
}
return $optimizersPath;
}
/**
* Resolves path to the internal prototypes.
*/
private function resolvePrototypesPath(): ?string
{
$prototypesPath = $this->prototypesPath;
// fallback
if (empty($prototypesPath)) {
$prototypesPath = dirname(__DIR__) . '/prototypes';
}
if (!is_dir($prototypesPath) || !is_readable($prototypesPath)) {
throw new IllegalStateException('Unable to resolve internal prototypes directory.');
}
return $prototypesPath;
}
private function toUnixPaths(array $paths): array
{
return array_map(
static fn(string $path): string => str_replace(DIRECTORY_SEPARATOR, '/', $path),
$paths
);
}
}
?>
Did this file decode correctly?
Original Code
<?php
/**
* This file is part of the Zephir.
*
* (c) Phalcon Team <[email protected]>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Zephir;
use DirectoryIterator;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use ReflectionException;
use Zephir\Backend\Backend;
use Zephir\Backend\FcallManagerInterface;
use Zephir\Backend\StringsManager;
use Zephir\Class\Definition\Definition;
use Zephir\Code\ArgInfoDefinition;
use Zephir\Code\Builder\Struct;
use Zephir\Code\Printer;
use Zephir\Compiler\CompilerFileFactory;
use Zephir\Compiler\FileInterface;
use Zephir\Exception\CompilerException;
use Zephir\Exception\IllegalStateException;
use Zephir\Exception\InvalidArgumentException;
use Zephir\Exception\NotImplementedException;
use Zephir\Exception\ParseException;
use Zephir\Exception\RuntimeException;
use Zephir\FileSystem\FileSystemInterface;
use Zephir\FileSystem\HardDisk;
use Zephir\Parser\Manager;
use function array_filter;
use function array_map;
use function array_merge;
use function array_unique;
use function asort;
use function basename;
use function call_user_func;
use function chmod;
use function class_exists;
use function count;
use function defined;
use function dirname;
use function exec;
use function explode;
use function extension_loaded;
use function file;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function fwrite;
use function getcwd;
use function getenv;
use function htmlentities;
use function implode;
use function in_array;
use function interface_exists;
use function is_array;
use function is_dir;
use function is_readable;
use function is_string;
use function krsort;
use function md5;
use function md5_file;
use function mkdir;
use function ob_get_clean;
use function ob_start;
use function phpinfo;
use function preg_match;
use function preg_replace;
use function realpath;
use function sprintf;
use function str_replace;
use function strcasecmp;
use function strip_tags;
use function strlen;
use function strpos;
use function strtolower;
use function strtoupper;
use function substr;
use function trim;
use function ucfirst;
use function unlink;
use function version_compare;
use const DIRECTORY_SEPARATOR;
use const INFO_GENERAL;
use const PHP_EOL;
use const PHP_INT_SIZE;
use const ZEND_THREAD_SAFE;
use const SORT_STRING;
use const STDERR;
final class Compiler
{
use LoggerAwareTrait;
/**
* @var FunctionDefinition[]
*/
public array $functionDefinitions = [];
private array $anonymousFiles = [];
private array $compiledFiles = [];
private array $constants = [];
/**
* @var Definition[]
*/
private array $definitions = [];
private array $externalDependencies = [];
private array $extraFiles = [];
private FcallManagerInterface $fcallManager;
/**
* @var CompilerFile[]
*/
private array $files = [];
private array $globals = [];
/**
* @var Definition[]
*/
private static array $internalDefinitions = [];
/**
* Additional initializer code.
* Used for static property initialization.
*/
private array $internalInitializers = [];
private static bool $loadedPrototypes = false;
private ?string $optimizersPath;
private ?string $prototypesPath;
private StringsManager $stringManager;
private ?string $templatesPath;
public function __construct(
private Config $config,
public Backend $backend,
private Manager $parserManager,
private FileSystemInterface $filesystem,
private CompilerFileFactory $compilerFileFactory,
) {
$this->logger = new NullLogger();
$this->stringManager = new StringsManager();
$this->fcallManager = $this->backend->getFcallManager();
try {
$this->assertRequiredExtensionsIsPresent();
} catch (RuntimeException $e) {
fwrite(STDERR, trim($e->getMessage()) . PHP_EOL);
exit(1);
}
}
/**
* Inserts an anonymous class definition in the compiler.
*/
public function addClassDefinition(CompilerFileAnonymous $file, Definition $classDefinition): void
{
$this->definitions[$classDefinition->getCompleteName()] = $classDefinition;
$this->anonymousFiles[$classDefinition->getCompleteName()] = $file;
}
/**
* Adds an external dependency to the compiler.
*/
public function addExternalDependency(string $namespace, string $location): void
{
$this->externalDependencies[$namespace] = $location;
}
/**
* Adds a function to the function definitions.
*/
public function addFunction(FunctionDefinition $func, array $statement = []): void
{
$funcName = strtolower($func->getInternalName());
if (isset($this->functionDefinitions[$funcName])) {
throw new CompilerException(
"Function '" . $func->getCompleteName() . "' was defined more than one time",
$statement
);
}
$this->functionDefinitions[$funcName] = $func;
}
/**
* Generate a HTML API.
*
* @throws ConfigException
* @throws Exception
* @throws ReflectionException
*/
public function api(array $options = [], bool $fromGenerate = false): void
{
if (!$fromGenerate) {
$this->generate();
}
$templatesPath = $this->templatesPath ?: dirname(__DIR__) . '/templates';
$documentator = new Documentation($this->files, $this->config, $templatesPath, $options);
$documentator->setLogger($this->logger);
$this->logger->info('Generating API into ' . $documentator->getOutputDirectory());
$documentator->build();
}
public function calculateDependencies(array $files, $_dependency = null): void
{
/**
* Classes are ordered according to a dependency ranking
* Classes with higher rank, need to be initialized first
* We first build a dependency tree and then set the rank accordingly
*/
if (null === $_dependency) {
$dependencyTree = [];
foreach ($files as $file) {
if (!$file->isExternal()) {
$classDefinition = $file->getClassDefinition();
$dependencyTree[$classDefinition->getCompleteName()] = $classDefinition->getDependencies();
}
}
// Make sure the dependencies are loaded first (recursively)
foreach ($dependencyTree as $dependencies) {
foreach ($dependencies as $dependency) {
$dependency->increaseDependencyRank(0);
$this->calculateDependencies($dependencyTree, $dependency);
}
}
return;
}
$dependencyTree = $files;
if (isset($dependencyTree[$_dependency->getCompleteName()])) {
foreach ($dependencyTree[$_dependency->getCompleteName()] as $dependency) {
$dependency->increaseDependencyRank(0);
$this->calculateDependencies($dependencyTree, $dependency);
}
}
}
/**
* Check if the project must be phpized again.
*
* @return bool
*/
public function checkIfPhpized(): bool
{
return !file_exists('ext/Makefile');
}
/**
* Compiles the extension without installing it.
*
* @param bool $development
* @param int|null $jobs
*
* @throws Exception
*/
public function compile(bool $development = false, int $jobs = null): void
{
$jobs = $jobs ?: 2;
/**
* Get global namespace.
*/
$namespace = str_replace('\\', '_', $this->checkDirectory());
$extensionName = $this->config->get('extension-name');
if (empty($extensionName) || !is_string($extensionName)) {
$extensionName = $namespace;
}
$currentDir = getcwd();
if (file_exists("$currentDir/compile.log")) {
unlink("$currentDir/compile.log");
}
if (file_exists("$currentDir/compile-errors.log")) {
unlink("$currentDir/compile-errors.log");
}
if (file_exists("$currentDir/ext/modules/{$namespace}.so")) {
unlink("$currentDir/ext/modules/{$namespace}.so");
}
if (Os::isWindows()) {
// TODO(klay): Make this better. Looks like it is non standard Env. Var
exec('cd ext && %PHP_DEVPACK%\\phpize --clean', $output, $exit);
$releaseFolder = $this->getWindowsReleaseDir();
if (file_exists($releaseFolder)) {
exec('rd /s /q ' . $releaseFolder, $output, $exit);
}
$this->logger->info('Preparing for PHP compilation...');
// TODO(klay): Make this better. Looks like it is non standard Env. Var
exec('cd ext && %PHP_DEVPACK%\\phpize', $output, $exit);
/**
* fix until patch hits all supported PHP builds.
*
* @see https://github.com/php/php-src/commit/9a3af83ee2aecff25fd4922ef67c1fb4d2af6201
*/
$fixMarker = '/* zephir_phpize_fix */';
$configureFile = file_get_contents('ext\\configure.js');
$configureFix = ["var PHP_ANALYZER = 'disabled';", "var PHP_PGO = 'no';", "var PHP_PGI = 'no';"];
$hasChanged = false;
if (!str_contains($configureFile, $fixMarker)) {
$configureFile = $fixMarker . PHP_EOL . implode(PHP_EOL, $configureFix) . PHP_EOL . $configureFile;
$hasChanged = true;
}
/* fix php's broken phpize patching ... */
$marker = 'var build_dir = (dirname ? dirname : "").replace(new RegExp("^..\\\\\\\\"), "");';
$pos = strpos($configureFile, $marker);
if (false !== $pos) {
$spMarker = 'if (MODE_PHPIZE) {';
$sp = strpos($configureFile, $spMarker, $pos - 200);
if (false === $sp) {
throw new CompilerException('outofdate... phpize seems broken again');
}
$configureFile = substr($configureFile, 0, $sp) .
'if (false) {' . substr($configureFile, $sp + strlen($spMarker));
$hasChanged = true;
}
if ($hasChanged) {
file_put_contents('ext\\configure.js', $configureFile);
}
$this->logger->info('Preparing configuration file...');
exec('cd ext && configure --enable-' . $extensionName);
} else {
exec('cd ext && make clean && phpize --clean', $output, $exit);
$this->logger->info('Preparing for PHP compilation...');
exec('cd ext && phpize', $output, $exit);
$this->logger->info('Preparing configuration file...');
exec(
'cd ext && export CC="gcc" && export CFLAGS="' .
$this->getGccFlags($development) .
'" && ./configure --enable-' .
$extensionName
);
}
$currentDir = getcwd();
$this->logger->info('Compiling...');
if (Os::isWindows()) {
exec(
'cd ext && nmake 2>' . $currentDir . '\compile-errors.log 1>' .
$currentDir . '\compile.log',
$output,
$exit
);
} else {
$this->preCompileHeaders();
exec(
'cd ext && (make -s -j' . $jobs . ' 2>' . $currentDir . '/compile-errors.log 1>' .
$currentDir .
'/compile.log)',
$output,
$exit
);
}
}
/**
* Create config.m4 and config.w32 for the extension.
*
* TODO: move this to backend?
*
* @throws Exception
*/
public function createConfigFiles(string $project): bool
{
$contentM4 = $this->backend->getTemplateFileContents('config.m4');
if (empty($contentM4)) {
throw new Exception("Template config.m4 doesn't exist");
}
$contentW32 = $this->backend->getTemplateFileContents('config.w32');
if (empty($contentW32)) {
throw new Exception("Template config.w32 doesn't exist");
}
$safeProject = 'zend' === $project ? 'zend_' : $project;
$compiledFiles = array_map(fn($file) => str_replace('.c', '.zep.c', $file), $this->compiledFiles);
/**
* If export-classes is enabled all headers are copied to include/php/ext.
*/
$exportClasses = $this->config->get('export-classes', 'extra');
if ($exportClasses) {
$compiledHeaders = array_map(fn($file) => str_replace('.c', '.zep.h', $file), $this->compiledFiles);
} else {
$compiledHeaders = ['php_' . strtoupper($project) . '.h'];
}
/**
* Check extra-libs, extra-cflags, package-dependencies exists
*/
$extraLibs = (string)$this->config->get('extra-libs');
$extraCflags = (string)$this->config->get('extra-cflags');
$contentM4 = $this->generatePackageDependenciesM4($contentM4);
$buildDirs = [];
foreach ($compiledFiles as $file) {
$dir = dirname($file);
if (!in_array($dir, $buildDirs)) {
$buildDirs[] = $dir;
}
}
asort($buildDirs);
/**
* Generate config.m4.
*/
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_CAMELIZE%' => ucfirst($project),
'%FILES_COMPILED%' => implode("\n\t", $this->toUnixPaths($compiledFiles)),
'%HEADERS_COMPILED%' => implode(' ', $this->toUnixPaths($compiledHeaders)),
'%EXTRA_FILES_COMPILED%' => implode("\n\t", $this->toUnixPaths($this->extraFiles)),
'%PROJECT_EXTRA_LIBS%' => $extraLibs,
'%PROJECT_EXTRA_CFLAGS%' => $extraCflags,
'%PROJECT_BUILD_DIRS%' => implode(' ', $buildDirs),
];
foreach ($toReplace as $mark => $replace) {
$contentM4 = str_replace($mark, $replace, $contentM4);
}
HardDisk::persistByHash($contentM4, 'ext/config.m4');
/**
* Generate config.w32.
*/
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%FILES_COMPILED%' => implode(
"\r\n\t",
$this->processAddSources($compiledFiles, strtolower($project))
),
'%EXTRA_FILES_COMPILED%' => implode(
"\r\n\t",
$this->processAddSources($this->extraFiles, strtolower($project))
),
];
foreach ($toReplace as $mark => $replace) {
$contentW32 = str_replace($mark, $replace, $contentW32);
}
$needConfigure = HardDisk::persistByHash($contentW32, 'ext/config.w32');
/**
* php_ext.h.
*/
$content = $this->backend->getTemplateFileContents('php_ext.h');
if (empty($content)) {
throw new Exception("Template php_ext.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/php_ext.h');
/**
* ext.h.
*/
$content = $this->backend->getTemplateFileContents('ext.h');
if (empty($content)) {
throw new Exception("Template ext.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/ext.h');
/**
* ext_config.h.
*/
$content = $this->backend->getTemplateFileContents('ext_config.h');
if (empty($content)) {
throw new Exception("Template ext_config.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER%' => strtolower($project),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/ext_config.h');
/**
* ext_clean.
*/
$content = $this->backend->getTemplateFileContents('clean');
if (empty($content)) {
throw new Exception("Clean file doesn't exist");
}
if (HardDisk::persistByHash($content, 'ext/clean')) {
chmod('ext/clean', 0755);
}
/**
* ext_install.
*/
$content = $this->backend->getTemplateFileContents('install');
if (empty($content)) {
throw new Exception("Install file doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER%' => strtolower($project),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
if (HardDisk::persistByHash($content, 'ext/install')) {
chmod('ext/install', 0755);
}
return (bool)$needConfigure;
}
/**
* Create project.c and project.h according to the current extension.
*
* TODO: Move the part of the logic which depends on templates (backend-specific) to backend?
*
* @throws Exception
*/
public function createProjectFiles(string $project): bool
{
$needConfigure = $this->checkKernelFiles();
/**
* project.c.
*/
$content = $this->backend->getTemplateFileContents('project.c');
if (empty($content)) {
throw new Exception("Template project.c doesn't exist");
}
$includes = '';
$reqInitializers = '';
$reqDestructors = '';
$prqDestructors = '';
$modInitializers = '';
$modDestructors = '';
$glbInitializers = '';
$glbDestructors = '';
$files = array_merge($this->files, $this->anonymousFiles);
/**
* Round 1. Calculate the dependency rank
*/
$this->calculateDependencies($files);
$classEntries = [];
$classInits = [];
$interfaceEntries = [];
$interfaceInits = [];
/**
* Round 2. Generate the ZEPHIR_INIT calls according to the dependency rank
*/
/** @var FileInterface $file */
foreach ($files as $file) {
if ($file->isExternal()) {
continue;
}
$classDefinition = $file->getClassDefinition();
if ($classDefinition === null) {
continue;
}
$dependencyRank = $classDefinition->getDependencyRank();
if ('class' === $classDefinition->getType()) {
$classEntries[$dependencyRank][] = 'zend_class_entry *' . $classDefinition->getClassEntry() . ';';
$classInits[$dependencyRank][] = 'ZEPHIR_INIT('
. $classDefinition->getCNamespace()
. '_'
. $classDefinition->getName()
. ');';
} else {
$interfaceEntries[$dependencyRank][] = 'zend_class_entry *' . $classDefinition->getClassEntry() . ';';
$interfaceInits[$dependencyRank][] = 'ZEPHIR_INIT('
. $classDefinition->getCNamespace()
. '_'
. $classDefinition->getName()
. ');';
}
}
krsort($classInits);
krsort($classEntries);
krsort($interfaceInits);
krsort($interfaceEntries);
$completeInterfaceInits = [];
foreach ($interfaceInits as $rankInterfaceInits) {
asort($rankInterfaceInits, SORT_STRING);
$completeInterfaceInits = array_merge($completeInterfaceInits, $rankInterfaceInits);
}
$completeInterfaceEntries = [];
foreach ($interfaceEntries as $rankInterfaceEntries) {
asort($rankInterfaceEntries, SORT_STRING);
$completeInterfaceEntries = array_merge($completeInterfaceEntries, $rankInterfaceEntries);
}
$completeClassInits = [];
foreach ($classInits as $rankClassInits) {
asort($rankClassInits, SORT_STRING);
$completeClassInits = array_merge($completeClassInits, $rankClassInits);
}
$completeClassEntries = [];
foreach ($classEntries as $rankClassEntries) {
asort($rankClassEntries, SORT_STRING);
$completeClassEntries = array_merge($completeClassEntries, $rankClassEntries);
}
/**
* Round 3. Process extension globals
*/
[$globalCode, $globalStruct, $globalsDefault, $initEntries] = $this->processExtensionGlobals($project);
if ('zend' == $project) {
$safeProject = 'zend_';
} else {
$safeProject = $project;
}
/**
* Round 4. Process extension info.
*/
$phpInfo = $this->processExtensionInfo();
/**
* Round 5. Generate Function entries (FE)
*/
[$feHeader, $feEntries] = $this->generateFunctionInformation();
/**
* Check if there are module/request/global destructors.
*/
$destructors = $this->config->get('destructors');
if (is_array($destructors)) {
$invokeRequestDestructors = $this->processCodeInjection($destructors, 'request');
$includes .= PHP_EOL . $invokeRequestDestructors[0];
$reqDestructors = $invokeRequestDestructors[1];
$invokePostRequestDestructors = $this->processCodeInjection($destructors, 'post-request');
$includes .= PHP_EOL . $invokePostRequestDestructors[0];
$prqDestructors = $invokePostRequestDestructors[1];
$invokeModuleDestructors = $this->processCodeInjection($destructors, 'module');
$includes .= PHP_EOL . $invokeModuleDestructors[0];
$modDestructors = $invokeModuleDestructors[1];
$invokeGlobalsDestructors = $this->processCodeInjection($destructors, 'globals');
$includes .= PHP_EOL . $invokeGlobalsDestructors[0];
$glbDestructors = $invokeGlobalsDestructors[1];
}
/**
* Check if there are module/request/global initializers.
*/
$initializers = $this->config->get('initializers');
if (is_array($initializers)) {
$invokeRequestInitializers = $this->processCodeInjection($initializers, 'request');
$includes .= PHP_EOL . $invokeRequestInitializers[0];
$reqInitializers = $invokeRequestInitializers[1];
$invokeModuleInitializers = $this->processCodeInjection($initializers, 'module');
$includes .= PHP_EOL . $invokeModuleInitializers[0];
$modInitializers = $invokeModuleInitializers[1];
$invokeGlobalsInitializers = $this->processCodeInjection($initializers, 'globals');
$includes .= PHP_EOL . $invokeGlobalsInitializers[0];
$glbInitializers = $invokeGlobalsInitializers[1];
}
/**
* Append extra details.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['init'])) {
$completeClassInits[] = 'ZEPHIR_INIT(' . $value['init'] . ')';
}
if (isset($value['entry'])) {
$completeClassEntries[] = 'zend_class_entry *' . $value['entry'] . ';';
}
}
}
$modRequires = array_map(
fn($mod) => sprintf('ZEND_MOD_REQUIRED("%s")', strtolower($mod)),
$this->config->get('extensions', 'requires') ?: []
);
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_CAMELIZE%' => ucfirst($project),
'%CLASS_ENTRIES%' => implode(
PHP_EOL,
array_merge($completeInterfaceEntries, $completeClassEntries)
),
'%CLASS_INITS%' => implode(
PHP_EOL . "\t",
array_merge($completeInterfaceInits, $completeClassInits)
),
'%INIT_GLOBALS%' => implode(
PHP_EOL . "\t",
array_merge((array)$globalsDefault[0], [$glbInitializers])
),
'%INIT_MODULE_GLOBALS%' => $globalsDefault[1],
'%DESTROY_GLOBALS%' => $glbDestructors,
'%EXTENSION_INFO%' => $phpInfo,
'%EXTRA_INCLUDES%' => implode(
PHP_EOL,
array_unique(explode(PHP_EOL, $includes))
),
'%MOD_INITIALIZERS%' => $modInitializers,
'%MOD_DESTRUCTORS%' => $modDestructors,
'%REQ_INITIALIZERS%' => implode(
PHP_EOL . "\t",
array_merge($this->internalInitializers, [$reqInitializers])
),
'%REQ_DESTRUCTORS%' => $reqDestructors,
'%POSTREQ_DESTRUCTORS%' => empty($prqDestructors) ? '' : implode(
PHP_EOL,
[
'#define ZEPHIR_POST_REQUEST 1',
'static PHP_PRSHUTDOWN_FUNCTION(' . strtolower($project) . ')',
'{',
"\t" . implode(
PHP_EOL . "\t",
explode(PHP_EOL, $prqDestructors)
),
'}',
]
),
'%FE_HEADER%' => $feHeader,
'%FE_ENTRIES%' => $feEntries,
'%PROJECT_INI_ENTRIES%' => implode(PHP_EOL . "\t", $initEntries),
'%PROJECT_DEPENDENCIES%' => implode(PHP_EOL . "\t", $modRequires),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
/**
* Round 5. Generate and place the entry point of the project
*/
HardDisk::persistByHash($content, 'ext/' . $safeProject . '.c');
unset($content);
/**
* Round 6. Generate the project main header.
*/
$content = $this->backend->getTemplateFileContents('project.h');
if (empty($content)) {
throw new Exception("Template project.h doesn't exists");
}
$includeHeaders = [];
foreach ($this->compiledFiles as $file) {
if ($file) {
$fileH = str_replace('.c', '.zep.h', $file);
$include = '#include "' . $fileH . '"';
$includeHeaders[] = $include;
}
}
/**
* Append extra headers.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['header'])) {
$include = '#include "' . $value['header'] . '"';
$includeHeaders[] = $include;
}
}
}
$toReplace = [
'%INCLUDE_HEADERS%' => implode(PHP_EOL, $includeHeaders),
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/' . $safeProject . '.h');
unset($content);
/**
* Round 7. Create php_project.h.
*/
$content = $this->backend->getTemplateFileContents('php_project.h');
if (empty($content)) {
throw new Exception("Template php_project.h doesn't exist");
}
$toReplace = [
'%PROJECT_LOWER_SAFE%' => strtolower($safeProject),
'%PROJECT_LOWER%' => strtolower($project),
'%PROJECT_UPPER%' => strtoupper($project),
'%PROJECT_EXTNAME%' => strtolower($project),
'%PROJECT_NAME%' => mb_convert_encoding($this->config->get('name'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_AUTHOR%' => mb_convert_encoding($this->config->get('author'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_VERSION%' => mb_convert_encoding($this->config->get('version'), 'ISO-8859-1', 'UTF-8'),
'%PROJECT_DESCRIPTION%' => mb_convert_encoding(
$this->config->get('description'),
'ISO-8859-1',
'UTF-8'
),
'%PROJECT_ZEPVERSION%' => Zephir::VERSION,
'%EXTENSION_GLOBALS%' => $globalCode,
'%EXTENSION_STRUCT_GLOBALS%' => $globalStruct,
];
foreach ($toReplace as $mark => $replace) {
$content = str_replace($mark, $replace, $content);
}
HardDisk::persistByHash($content, 'ext/php_' . $safeProject . '.h');
unset($content);
return $needConfigure;
}
/**
* Generates the C sources from Zephir without compiling them.
*
* @throws Exception
* @throws ReflectionException
*/
public function generate(bool $fromGenerate = false): bool
{
/**
* Get global namespace.
*/
$namespace = $this->checkDirectory();
/**
* Check whether there are external dependencies.
*/
$externalDependencies = $this->config->get('external-dependencies');
if (is_array($externalDependencies)) {
foreach ($externalDependencies as $dependencyNs => $location) {
if (!file_exists($location)) {
throw new CompilerException(
sprintf(
'Location of dependency "%s" does not exist. Check the config.json for more information.',
$dependencyNs
)
);
}
$this->addExternalDependency($dependencyNs, $location);
}
}
/**
* Round 1. pre-compile all files in memory
*/
$this->recursivePreCompile(str_replace('\\', DIRECTORY_SEPARATOR, $namespace));
if (!count($this->files)) {
throw new Exception(
"Zephir files to compile couldn't be found. Did you add a first class to the extension?"
);
}
/**
* Round 2. Check 'extends' and 'implements' dependencies
*/
foreach ($this->files as $compileFile) {
$compileFile->checkDependencies($this);
}
/**
* Sort the files by dependency ranking.
*/
$files = [];
$rankedFiles = [];
$this->calculateDependencies($this->files);
foreach ($this->files as $rankFile) {
$rank = $rankFile->getClassDefinition()->getDependencyRank();
$rankedFiles[$rank][] = $rankFile;
}
krsort($rankedFiles);
foreach ($rankedFiles as $rankFiles) {
$files = array_merge($files, $rankFiles);
}
$this->files = $files;
/**
* Convert C-constants into PHP constants.
*/
$constantsSources = $this->config->get('constants-sources');
if (is_array($constantsSources)) {
$this->loadConstantsSources($constantsSources);
}
/**
* Set extension globals.
*/
$globals = $this->config->get('globals');
if (is_array($globals)) {
$this->setExtensionGlobals($globals);
}
/**
* Load function optimizers
*/
if (false === self::$loadedPrototypes) {
$optimizersPath = $this->resolveOptimizersPath();
FunctionCall::addOptimizerDir("{$optimizersPath}/FunctionCall");
$customOptimizersPaths = $this->config->get('optimizer-dirs');
if (is_array($customOptimizersPaths)) {
foreach ($customOptimizersPaths as $directory) {
FunctionCall::addOptimizerDir(realpath($directory));
}
}
/**
* Load additional extension prototypes.
*/
$prototypesPath = $this->resolvePrototypesPath();
foreach (new DirectoryIterator($prototypesPath) as $file) {
if ($file->isDir() || $file->isDot()) {
continue;
}
// Do not use $file->getRealPath() because it does not work inside phar
$realPath = "{$file->getPath()}/{$file->getFilename()}";
$extension = $file->getBasename(".{$file->getExtension()}");
if (!extension_loaded($extension)) {
require_once $realPath;
}
}
/**
* Load customer additional extension prototypes.
*/
$prototypeDirs = $this->config->get('prototype-dir');
if (is_array($prototypeDirs)) {
foreach ($prototypeDirs as $prototype => $prototypeDir) {
/**
* Check if the extension is installed
*/
if (!extension_loaded($prototype)) {
$prototypeRealpath = realpath($prototypeDir);
if ($prototypeRealpath) {
foreach (new RecursiveDirectoryIterator($prototypeRealpath) as $file) {
if ($file->isDir()) {
continue;
}
require_once $file->getRealPath();
}
}
}
}
}
self::$loadedPrototypes = true;
}
/**
* Round 3. Compile all files to C sources.
*/
$files = [];
$hash = '';
foreach ($this->files as $compileFile) {
/**
* Only compile classes in the local extension, ignore external classes
*/
if (!$compileFile->isExternal()) {
$compileFile->compile($this, $this->stringManager);
$compiledFile = $compileFile->getCompiledFile();
$methods = [];
$classDefinition = $compileFile->getClassDefinition();
foreach ($classDefinition->getMethods() as $method) {
$methods[] = '[' . $method->getName() . ':' . implode('-', $method->getVisibility()) . ']';
if ($method->isInitializer() && $method->isStatic()) {
$this->internalInitializers[] = "\t" . $method->getName() . '();';
}
}
$files[] = $compiledFile;
$hash .= '|'
. $compiledFile
. ':'
. $classDefinition->getClassEntry()
. '['
. implode('|', $methods)
. ']';
}
}
/**
* Round 3.2. Compile anonymous classes
*/
foreach ($this->anonymousFiles as $compileFile) {
$compileFile->compile($this, $this->stringManager);
$compiledFile = $compileFile->getCompiledFile();
$methods = [];
$classDefinition = $compileFile->getClassDefinition();
foreach ($classDefinition->getMethods() as $method) {
$methods[] = '['
. $method->getName()
. ':'
. implode('-', $method->getVisibility())
. ']';
}
$files[] = $compiledFile;
$hash .= '|'
. $compiledFile
. ':'
. $classDefinition->getClassEntry()
. '['
. implode('|', $methods)
. ']';
}
$hash = md5($hash);
$this->compiledFiles = $files;
/**
* Round 3.3. Load extra C-sources.
*/
$extraSources = $this->config->get('extra-sources');
if (is_array($extraSources)) {
$this->extraFiles = $extraSources;
} else {
$this->extraFiles = [];
}
/**
* Round 3.4. Load extra classes sources.
*/
$extraClasses = $this->config->get('extra-classes');
if (is_array($extraClasses)) {
foreach ($extraClasses as $value) {
if (isset($value['source'])) {
$this->extraFiles[] = $value['source'];
}
}
}
/**
* Round 4. Create config.m4 and config.w32 files / Create project.c and project.h files.
*/
$namespace = str_replace('\\', '_', $namespace);
$extensionName = $this->config->get('extension-name');
if (empty($extensionName) || !is_string($extensionName)) {
$extensionName = $namespace;
}
$needConfigure = $this->createConfigFiles($extensionName);
$needConfigure |= $this->createProjectFiles($extensionName);
$needConfigure |= $this->checkIfPhpized();
// Bitwise returns `int` instead of `bool`.
$needConfigure = (bool)$needConfigure;
/**
* When a new file is added or removed we need to run configure again
*/
if (!$fromGenerate) {
if (false === $this->filesystem->exists('compiled-files-sum')) {
$needConfigure = true;
$this->filesystem->write('compiled-files-sum', $hash);
} else {
if ($this->filesystem->read('compiled-files-sum') != $hash) {
$needConfigure = true;
$this->filesystem->delete('compiled-files-sum');
$this->filesystem->write('compiled-files-sum', $hash);
}
}
}
/**
* Round 5. Generate concatenation functions
*/
$this->stringManager->genConcatCode();
$this->fcallManager->genFcallCode();
if ($this->config->get('stubs-run-after-generate', 'stubs')) {
$this->stubs($fromGenerate);
}
return $needConfigure;
}
public function generateFunctionInformation(): array
{
$headerPrinter = new Printer();
$entryPrinter = new Printer();
/**
* Specifying Argument Information
*/
foreach ($this->functionDefinitions as $func) {
$argInfo = new ArgInfoDefinition(
$func->getArgInfoName(),
$func,
$headerPrinter,
$func->getCallGathererPass()->getCompilationContext()
);
$funcName = $func->getInternalName();
$argInfoName = $func->getArgInfoName();
$headerPrinter->output('PHP_FUNCTION(' . $funcName . ');');
$argInfo->setBooleanDefinition('_IS_BOOL');
$argInfo->setRichFormat(true);
$argInfo->render();
/** Generate FE's */
$paramData = 'NULL';
$richFormat = $func->isReturnTypesHintDetermined() && $func->areReturnTypesCompatible();
if ($richFormat || $func->hasParameters()) {
$paramData = $argInfoName;
}
if ($func->isGlobal()) {
$entryPrinter->output(
'ZEND_NAMED_FE(' . $func->getName() . ', ZEND_FN(' . $funcName . '), ' . $paramData . ')'
);
} else {
$entryPrinter->output(
'ZEND_NS_NAMED_FE("' . str_replace('\\', '\\\\', $func->getNamespace()) . '", ' .
$func->getName() .
', ZEND_FN(' . $funcName . '), ' .
$paramData . ')'
);
}
}
$entryPrinter->output('ZEND_FE_END');
return [$headerPrinter->getOutput(), $entryPrinter->getOutput()];
}
/**
* Generate package-dependencies config for m4.
*
* TODO: Move the template depending part to backend?
*/
public function generatePackageDependenciesM4(string $contentM4): string
{
$packageDependencies = $this->config->get('package-dependencies');
if (!is_array($packageDependencies)) {
return str_replace('%PROJECT_PACKAGE_DEPENDENCIES%', '', $contentM4);
}
$pkgconfigM4 = $this->backend->getTemplateFileContents('pkg-config.m4');
$pkgconfigCheckM4 = $this->backend->getTemplateFileContents('pkg-config-check.m4');
$extraCFlags = '';
foreach ($packageDependencies as $pkg => $version) {
$pkgM4Buf = $pkgconfigCheckM4;
$operator = '=';
$operatorCmd = '--exact-version';
$ar = explode('=', $version);
if (1 === count($ar)) {
if ('*' === $version) {
$version = '0.0.0';
$operator = '>=';
$operatorCmd = '--atleast-version';
}
} else {
switch ($ar[0]) {
case '<':
$operator = '<=';
$operatorCmd = '--max-version';
break;
case '>':
$operator = '>=';
$operatorCmd = '--atleast-version';
break;
}
$version = trim($ar[1]);
}
$toReplace = [
'%PACKAGE_LOWER%' => strtolower($pkg),
'%PACKAGE_UPPER%' => strtoupper($pkg),
'%PACKAGE_REQUESTED_VERSION%' => $operator . ' ' . $version,
'%PACKAGE_PKG_CONFIG_COMPARE_VERSION%' => $operatorCmd . '=' . $version,
];
foreach ($toReplace as $mark => $replace) {
$pkgM4Buf = str_replace($mark, $replace, $pkgM4Buf);
}
$pkgconfigM4 .= $pkgM4Buf;
$extraCFlags .= '$PHP_' . strtoupper($pkg) . '_INCS ';
}
$contentM4 = str_replace('%PROJECT_EXTRA_CFLAGS%', '%PROJECT_EXTRA_CFLAGS% ' . $extraCFlags, $contentM4);
return str_replace('%PROJECT_PACKAGE_DEPENDENCIES%', $pkgconfigM4, $contentM4);
}
/**
* Returns class the class definition from a given class name.
*/
public function getClassDefinition(string $className): Definition | bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className)) {
return $value;
}
}
return false;
}
/**
* Returns a Zephir Constant by its name.
*/
public function getConstant(string $name): mixed
{
return $this->constants[$name];
}
/**
* Returns an extension global by its name.
*/
public function getExtensionGlobal(string $name): array
{
return $this->globals[$name];
}
/**
* Returns GCC flags for current compilation.
*/
public function getGccFlags(bool $development = false): string
{
if (Os::isWindows()) {
// TODO
return '';
}
$gccFlags = getenv('CFLAGS');
if (!is_string($gccFlags)) {
if (false === $development) {
$gccVersion = $this->getGccVersion();
if (version_compare($gccVersion, '4.6.0', '>=')) {
$gccFlags = '-O2 -fvisibility=hidden -Wparentheses -flto -DZEPHIR_RELEASE=1';
} else {
$gccFlags = '-O2 -fvisibility=hidden -Wparentheses -DZEPHIR_RELEASE=1';
}
} else {
$gccFlags = '-O0 -g3';
}
}
return $gccFlags;
}
/**
* Returns class the class definition from a given class name.
*
* @throws ReflectionException
*/
public function getInternalClassDefinition(string $className): Definition
{
if (!isset(self::$internalDefinitions[$className])) {
$reflection = new ReflectionClass($className);
self::$internalDefinitions[$className] = Definition::buildFromReflection($reflection);
}
return self::$internalDefinitions[$className];
}
/**
* Gets the Zephir Parser Manager.
*
* @deprecated
*/
public function getParserManager(): Parser\Manager
{
return $this->parserManager;
}
/**
* Returns the php include directories returned by php-config.
*/
public function getPhpIncludeDirs(): string
{
$this->filesystem->system('php-config --includes', 'stdout', 'php-includes');
return trim($this->filesystem->read('php-includes'));
}
/**
* Returns a short user path.
*/
public static function getShortUserPath(string $path): string
{
return str_replace('\\', '/', str_replace(getcwd() . DIRECTORY_SEPARATOR, '', $path));
}
/**
* Compiles and installs the extension.
*
* @throws Exception
* @throws NotImplementedException
* @throws CompilerException
*/
public function install(bool $development = false): void
{
// Get global namespace
$namespace = str_replace('\\', '_', $this->checkDirectory());
$currentDir = getcwd();
if (Os::isWindows()) {
throw new NotImplementedException('Installation is not implemented for Windows yet. Aborting.');
}
$this->logger->info('Installing...');
$gccFlags = $this->getGccFlags($development);
$command = strtr(
// TODO: Sort out with sudo
'cd ext && export CC="gcc" && export CFLAGS=":cflags" && ' .
'make 2>> ":stderr" 1>> ":stdout" && ' .
'sudo make install 2>> ":stderr" 1>> ":stdout"',
[
':cflags' => $gccFlags,
':stderr' => "{$currentDir}/compile-errors.log",
':stdout' => "{$currentDir}/compile.log",
]
);
array_map(function ($entry): void {
if (!empty($entry)) {
$this->logger->debug(trim($entry));
}
}, explode('&&', $command));
exec($command, $output, $exit);
$fileName = $this->config->get('extension-name') ?: $namespace;
if (false === file_exists("{$currentDir}/ext/modules/{$fileName}.so")) {
throw new CompilerException(
'Internal extension compilation failed. Check compile-errors.log for more information.'
);
}
}
/**
* Allows checking if a class is part of PHP.
*/
public function isBundledClass(string $className): bool
{
return class_exists($className, false);
}
/**
* Allows checking if an interface is part of PHP.
*/
public function isBundledInterface(string $className): bool
{
return interface_exists($className, false);
}
/**
* Allows to check if a class is part of the compiled extension.
*/
public function isClass(string $className): bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className) && 'class' === $value->getType()) {
return true;
}
}
/**
* Try to autoload the class from an external dependency
*/
foreach ($this->externalDependencies as $namespace => $location) {
if (preg_match('#^' . $namespace . '\\\\#i', $className)) {
return $this->loadExternalClass($className, $location);
}
}
return false;
}
/**
* Checks if $name is a Zephir constant.
*/
public function isConstant(string $name): bool
{
return isset($this->constants[$name]);
}
/**
* Checks if a specific extension global is defined.
*/
public function isExtensionGlobal(string $name): bool
{
return isset($this->globals[$name]);
}
/**
* Allows checking if an interface is part of the compiled extension.
*
* @throws CompilerException
* @throws IllegalStateException
* @throws ParseException
*/
public function isInterface(string $className): bool
{
foreach ($this->definitions as $key => $value) {
if (!strcasecmp($key, $className) && Definition::TYPE_INTERFACE === $value->getType()) {
return true;
}
}
/**
* Try to autoload the class from an external dependency
*/
foreach ($this->externalDependencies as $namespace => $location) {
if (preg_match('#^' . $namespace . '\\\\#i', $className)) {
return $this->loadExternalClass($className, $location);
}
}
return false;
}
/**
* Loads a class definition in an external dependency.
*
* @throws CompilerException
* @throws IllegalStateException
* @throws ParseException
*/
public function loadExternalClass(string $className, string $location): bool
{
$filePath = $location
. DIRECTORY_SEPARATOR
. strtolower(
str_replace('\\', DIRECTORY_SEPARATOR, $className)
)
. '.zep';
/**
* Fix the class name.
*/
$className = implode(
'\\',
array_map(
'ucfirst',
explode('\\', $className)
)
);
if (isset($this->files[$className])) {
return true;
}
if (!file_exists($filePath)) {
return false;
}
/** @var CompilerFile|CompilerFileAnonymous $compilerFile */
$compilerFile = $this->compilerFileFactory->create($className, $filePath);
$compilerFile->setIsExternal(true);
$compilerFile->preCompile($this);
$this->files[$className] = $compilerFile;
$this->definitions[$className] = $compilerFile->getClassDefinition();
return true;
}
/**
* Pre-compile headers to speed up compilation.
*/
public function preCompileHeaders(): void
{
if (Os::isWindows()) {
// TODO: Add Windows support
return;
}
$phpIncludes = $this->getPhpIncludeDirs();
/** @var DirectoryIterator $file */
foreach (new DirectoryIterator('ext/kernel') as $file) {
if ($file->isDir() || $file->getExtension() !== 'h') {
continue;
}
$command = sprintf(
'cd ext && gcc -c kernel/%s -I. %s -o kernel/%s.gch',
$file->getBaseName(),
$phpIncludes,
$file->getBaseName()
);
$path = $file->getRealPath();
if (!file_exists($path . '.gch') || filemtime($path) > filemtime($path . '.gch')) {
$this->filesystem->system($command, 'stdout', 'compile-header');
}
}
}
/**
* Process extension code injection.
*/
public function processCodeInjection(array $entries, string $section = 'request'): array
{
$codes = [];
$includes = [];
if (isset($entries[$section])) {
foreach ($entries[$section] as $entry) {
if (!empty($entry['code'])) {
$codes[] = $entry['code'] . ';';
}
if (!empty($entry['include'])) {
$includes[] = '#include "' . $entry['include'] . '"';
}
}
}
return [implode(PHP_EOL, $includes), implode("\n\t", $codes)];
}
/**
* Process extension globals.
*
* @throws Exception
*/
public function processExtensionGlobals(string $namespace): array
{
$globalCode = '';
$globalStruct = '';
$globalsDefault = [[], []];
$initEntries = [];
/**
* Generate the extensions globals declaration.
*/
$globals = $this->config->get('globals');
if (is_array($globals)) {
$structures = [];
$variables = [];
foreach ($globals as $name => $global) {
$parts = explode('.', $name);
if (isset($parts[1])) {
$structures[$parts[0]][$parts[1]] = $global;
} else {
$variables[$parts[0]] = $global;
}
}
/**
* Process compound structures
*/
foreach ($structures as $structureName => $internalStructure) {
if (preg_match('/^[0-9a-zA-Z_]$/', $structureName)) {
throw new Exception("Struct name: '" . $structureName . "' contains invalid characters");
}
$structBuilder = new Struct('_zephir_struct_' . $structureName, $structureName);
foreach ($internalStructure as $field => $global) {
if (preg_match('/^[0-9a-zA-Z_]$/', $field)) {
throw new Exception("Struct field name: '" . $field . "' contains invalid characters");
}
$structBuilder->addProperty($field, $global['type']);
$isModuleGlobal = (int)!empty($global['module']);
$globalsDefault[$isModuleGlobal][] = $structBuilder->getCDefault($field, $global, $namespace);
$initEntries[] = $structBuilder->getInitEntry($field, $global, $namespace);
}
$globalStruct .= $structBuilder . PHP_EOL;
}
$globalCode = PHP_EOL;
foreach ($structures as $structureName => $internalStructure) {
$globalCode .= "\t" . 'zephir_struct_' . $structureName . ' ' . $structureName . ';' . PHP_EOL;
}
/**
* Process single variables
*/
foreach ($variables as $name => $global) {
if (preg_match('/^[0-9a-zA-Z_]$/', $name)) {
throw new Exception("Extension global variable name: '" . $name . "' contains invalid characters");
}
if (!isset($global['default'])) {
throw new Exception("Extension global variable name: '" . $name . "' contains invalid characters");
}
$isModuleGlobal = (int)!empty($global['module']);
$type = $global['type'];
// TODO: Add support for 'hash'
// TODO: Zephir\Optimizers\FunctionCall\GlobalsSetOptimizer
switch ($global['type']) {
case 'boolean':
case 'bool':
$type = 'zend_bool';
if (true === $global['default']) {
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = 1;';
} else {
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = 0;';
}
break;
case 'int':
case 'uint':
case 'long':
case 'double':
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = ' . $global['default'] . ';';
break;
case 'char':
case 'uchar':
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = \'' . $global['default'] . '\';';
break;
case 'string':
$type = 'char *';
$globalsDefault[$isModuleGlobal][] = "\t" . $namespace . '_globals->' . $name . ' = ZSTR_VAL(zend_string_init(ZEND_STRL("' . $global['default'] . '"), 0));';
break;
default:
throw new Exception(
"Unknown type '" . $global['type'] . "' for extension global '" . $name . "'"
);
}
$globalCode .= "\t" . $type . ' ' . $name . ';' . PHP_EOL;
$iniEntry = $global['ini-entry'] ?? [];
$iniName = $iniEntry['name'] ?? $namespace . '.' . $name;
$scope = $iniEntry['scope'] ?? 'PHP_INI_ALL';
switch ($global['type']) {
case 'boolean':
case 'bool':
$initEntries[] =
'STD_PHP_INI_BOOLEAN("' .
$iniName .
'", "' .
(int)(true === $global['default']) .
'", ' .
$scope .
', OnUpdateBool, ' .
$name .
', zend_' .
$namespace .
'_globals, ' .
$namespace . '_globals)';
break;
case 'string':
$initEntries[] = sprintf(
'STD_PHP_INI_ENTRY(%s, %s, %s, NULL, %s, %s, %s)',
'"' . $iniName . '"',
'"' . $global['default'] . '"',
$scope,
$name,
'zend_' . $namespace . '_globals',
$namespace . '_globals',
);
break;
}
}
}
$globalsDefault[0] = implode(PHP_EOL, $globalsDefault[0]);
$globalsDefault[1] = implode(PHP_EOL, $globalsDefault[1]);
return [$globalCode, $globalStruct, $globalsDefault, $initEntries];
}
/**
* Generates phpinfo() sections showing information about the extension.
*/
public function processExtensionInfo(): string
{
$phpinfo = '';
$info = $this->config->get('info');
if (!is_array($info)) {
return $phpinfo;
}
foreach ($info as $table) {
$phpinfo .= "\t" . 'php_info_print_table_start();' . PHP_EOL;
if (isset($table['header'])) {
$headerArray = [];
foreach ($table['header'] as $header) {
$headerArray[] = '"' . htmlentities($header) . '"';
}
$phpinfo .= "\t" . 'php_info_print_table_header(' . count($headerArray) . ', ' .
implode(', ', $headerArray) . ');' . PHP_EOL;
}
if (isset($table['rows'])) {
foreach ($table['rows'] as $row) {
$rowArray = [];
foreach ($row as $field) {
$rowArray[] = '"' . htmlentities($field) . '"';
}
$phpinfo .= "\t" . 'php_info_print_table_row(' . count($rowArray) . ', ' .
implode(', ', $rowArray) . ');' . PHP_EOL;
}
}
$phpinfo .= "\t" . 'php_info_print_table_end();' . PHP_EOL;
}
return $phpinfo;
}
/**
* Sets extensions globals.
*/
public function setExtensionGlobals(array $globals): void
{
foreach ($globals as $key => $value) {
$this->globals[$key] = $value;
}
}
public function setOptimizersPath(string $optimizersPath): void
{
$this->optimizersPath = $optimizersPath;
}
public function setPrototypesPath(string $prototypesPath): void
{
$this->prototypesPath = $prototypesPath;
}
public function setTemplatesPath(string $templatesPath): void
{
$this->templatesPath = $templatesPath;
}
/**
* Generate IDE stubs.
*
* @throws Exception
* @throws ReflectionException
*/
public function stubs(bool $fromGenerate = false): void
{
if (!$fromGenerate) {
$this->generate();
}
$this->logger->info('Generating stubs...');
$path = str_replace(
[
'%version%',
'%namespace%',
],
[
$this->config->get('version'),
ucfirst($this->config->get('namespace')),
],
$this->config->get('path', 'stubs')
);
(new Stubs\Generator($this->files))->generate(
$this->config->get('namespace'),
$path,
$this->config->get('indent', 'extra'),
$this->config->get('banner', 'stubs') ?? ''
);
}
/**
* Ensure that required extensions is present.
*
* @throws RuntimeException
*/
private function assertRequiredExtensionsIsPresent(): void
{
$extensionRequires = $this->config->get('extensions', 'requires');
if (empty($extensionRequires)) {
return;
}
$extensions = [];
foreach ($extensionRequires as $value) {
// TODO: We'll use this as an object in the future.
if (!is_string($value)) {
continue;
}
if (!extension_loaded($value)) {
$extensions[] = $value;
}
}
if (!empty($extensions)) {
throw new RuntimeException(
sprintf(
'Could not load extension(s): %s. You must load extensions above before build this extension.',
implode(', ', $extensions)
)
);
}
}
/**
* Checks if the current directory is a valid Zephir project.
*
* @throws Exception
*/
private function checkDirectory(): string
{
$namespace = $this->config->get('namespace');
if (!$namespace) {
// TODO: Add more user friendly message.
// For example assume if the user call the command from the wrong dir
throw new Exception('Extension namespace cannot be loaded');
}
if (!is_string($namespace)) {
throw new Exception('Extension namespace is invalid');
}
if (!$this->filesystem->isInitialized()) {
$this->filesystem->initialize();
}
if (!$this->filesystem->exists('.')) {
if (!$this->checkIfPhpized()) {
$this->logger->info(
'Zephir version has changed, use "zephir fullclean" to perform a full clean of the project'
);
}
$this->filesystem->makeDirectory('.');
}
return $namespace;
}
/**
* Checks if a file must be copied.
*
* @param string $src
* @param string $dst
*
* @return bool
*/
private function checkKernelFile(string $src, string $dst): bool
{
if (preg_match('#kernels/ZendEngine[2-9]/concat\.#', $src)) {
return true;
}
if (!file_exists($dst)) {
return false;
}
return md5_file($src) === md5_file($dst);
}
/**
* Checks which files in the base kernel must be copied.
*
* @throws Exception
*/
private function checkKernelFiles(): bool
{
$kernelPath = 'ext' . DIRECTORY_SEPARATOR . 'kernel';
if (!file_exists($kernelPath)) {
if (!mkdir($kernelPath, 0775, true)) {
throw new Exception("Cannot create kernel directory: {$kernelPath}");
}
}
$kernelPath = realpath($kernelPath);
$sourceKernelPath = $this->backend->getInternalKernelPath();
$configured = $this->recursiveProcess(
$sourceKernelPath,
$kernelPath,
'@.*\.[ch]$@',
[$this, 'checkKernelFile']
);
if (!$configured) {
$this->logger->info('Cleaning old kernel files...');
$this->recursiveDeletePath($kernelPath, '@^.*\.[lcho]$@');
@mkdir($kernelPath);
$this->logger->info('Copying new kernel files...');
$this->recursiveProcess($sourceKernelPath, $kernelPath, '@^.*\.[ch]$@');
}
return !$configured;
}
/**
* Returns current GCC version.
*/
private function getGccVersion(): string
{
if (Os::isWindows()) {
return '0.0.0';
}
if ($this->filesystem->exists('gcc-version')) {
return $this->filesystem->read('gcc-version');
}
$this->filesystem->system('gcc -dumpversion', 'stdout', 'gcc-version');
$lines = $this->filesystem->file('gcc-version');
$lines = array_filter($lines);
$lastLine = $lines[count($lines) - 1];
if (preg_match('/\d+\.\d+\.\d+/', $lastLine, $matches)) {
return $matches[0];
}
return '0.0.0';
}
private function getWindowsReleaseDir(): string
{
if ($this->isZts()) {
if (PHP_INT_SIZE === 4) {
// 32-bit version of PHP
return 'ext\\Release_TS';
}
if (PHP_INT_SIZE === 8) {
// 64-bit version of PHP
return 'ext\\x64\\Release_TS';
}
// fallback
return 'ext\\Release_TS';
}
if (PHP_INT_SIZE === 4) {
// 32-bit version of PHP
return 'ext\\Release';
}
if (PHP_INT_SIZE === 8) {
// 64-bit version of PHP
return 'ext\\x64\\Release';
}
// fallback
return 'ext\\Release';
}
private function isZts(): bool
{
if (defined('ZEND_THREAD_SAFE') && ZEND_THREAD_SAFE === true) {
return true;
}
ob_start();
phpinfo(INFO_GENERAL);
return (bool)preg_match('/Thread\s*Safety\s*enabled/i', strip_tags(ob_get_clean()));
}
/**
* Registers C-constants as PHP constants from a C-file.
*
* @param array $constantsSources
*
* @throws Exception
*/
private function loadConstantsSources(array $constantsSources): void
{
foreach ($constantsSources as $constantsSource) {
if (!file_exists($constantsSource)) {
throw new Exception("File '" . $constantsSource . "' with constants definitions");
}
foreach (file($constantsSource) as $line) {
if (preg_match('/^\#define[ \t]+([A-Z0-9\_]+)[ \t]+([0-9]+)/', $line, $matches)) {
$this->constants[$matches[1]] = ['int', $matches[2]];
continue;
}
if (preg_match('/^\#define[ \t]+([A-Z0-9\_]+)[ \t]+(\'(.){1}\')/', $line, $matches)) {
$this->constants[$matches[1]] = ['char', $matches[3]];
}
}
}
}
/**
* Pre-compiles classes creating a CompilerFile definition.
*
* @throws IllegalStateException
*/
private function preCompile(string $filePath): void
{
if (!$this->parserManager->isAvailable()) {
throw new IllegalStateException($this->parserManager->requirements());
}
if (preg_match('#\.zep$#', $filePath)) {
$className = str_replace(DIRECTORY_SEPARATOR, '\\', $filePath);
$className = preg_replace('#.zep$#', '', $className);
$className = implode('\\', array_map('ucfirst', explode('\\', $className)));
$compilerFile = $this->compilerFileFactory->create($className, $filePath);
$compilerFile->preCompile($this);
$this->files[$className] = $compilerFile;
$this->definitions[$className] = $compilerFile->getClassDefinition();
}
}
/**
* Process config.w32 sections.
*
* @param array $sources
* @param string $project
*
* @return array
*/
private function processAddSources(array $sources, string $project): array
{
$groupSources = [];
foreach ($sources as $source) {
$dirName = str_replace(DIRECTORY_SEPARATOR, '/', dirname($source));
if (!isset($groupSources[$dirName])) {
$groupSources[$dirName] = [];
}
$groupSources[$dirName][] = basename($source);
}
$groups = [];
foreach ($groupSources as $dirname => $files) {
$groups[] = 'ADD_SOURCES(configure_module_dirname + "/'
. $dirname
. '", "'
. implode(' ', $files)
. '", "'
. $project
. '");';
}
return $groups;
}
/**
* Recursively deletes files in a specified location.
*
* @param string $path Directory to deletes files
* @param string $mask Regular expression to deletes files
*
* @deprecated
*
*/
private function recursiveDeletePath($path, $mask): void
{
if (!file_exists($path) || !is_dir($path) || !is_readable($path)) {
$this->logger->warning("Directory '{$path}' is not readable. Skip...");
return;
}
$objects = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($objects as $name => $object) {
if (preg_match($mask, $name)) {
@unlink($name);
}
}
}
/**
* Recursively pre-compiles all sources found in the given path.
*
* @throws IllegalStateException
* @throws InvalidArgumentException
*/
private function recursivePreCompile(string $path): void
{
if (!is_dir($path)) {
throw new InvalidArgumentException(
sprintf(
"An invalid path was passed to the compiler. Unable to obtain the '%s%s%s' directory.",
getcwd(),
DIRECTORY_SEPARATOR,
$path
)
);
}
/**
* Pre compile all files.
*/
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path),
RecursiveIteratorIterator::SELF_FIRST
);
$files = [];
foreach ($iterator as $item) {
if (!$item->isDir()) {
$files[] = $item->getPathname();
}
}
sort($files, SORT_STRING);
foreach ($files as $file) {
$this->preCompile($file);
}
}
/**
* Copies the base kernel to the extension destination.
*
* TODO:
*
* @param $src
* @param $dest
* @param string $pattern
* @param mixed $callback
*
* @return bool
* @deprecated
*
*/
private function recursiveProcess($src, $dest, $pattern = null, $callback = 'copy')
{
$success = true;
$iterator = new DirectoryIterator($src);
foreach ($iterator as $item) {
$pathName = $item->getPathname();
if (!is_readable($pathName)) {
$this->logger->warning('File is not readable :' . $pathName);
continue;
}
$fileName = $item->getFileName();
if ($item->isDir()) {
if ('.' != $fileName && '..' != $fileName && '.libs' != $fileName) {
if (!is_dir($dest . DIRECTORY_SEPARATOR . $fileName)) {
mkdir($dest . DIRECTORY_SEPARATOR . $fileName, 0755, true);
}
$this->recursiveProcess($pathName, $dest . DIRECTORY_SEPARATOR . $fileName, $pattern, $callback);
}
} elseif (!$pattern || ($pattern && 1 === preg_match($pattern, $fileName))) {
$path = $dest . DIRECTORY_SEPARATOR . $fileName;
$success = $success && call_user_func($callback, $pathName, $path);
}
}
return $success;
}
/**
* Resolves path to the internal optimizers.
*
* @throws IllegalStateException in case of absence internal optimizers directory
*/
private function resolveOptimizersPath(): ?string
{
$optimizersPath = $this->optimizersPath;
// fallback
if (empty($optimizersPath)) {
$optimizersPath = __DIR__ . '/Optimizers';
}
if (!is_dir($optimizersPath) || !is_readable($optimizersPath)) {
throw new IllegalStateException('Unable to resolve internal optimizers directory.');
}
return $optimizersPath;
}
/**
* Resolves path to the internal prototypes.
*/
private function resolvePrototypesPath(): ?string
{
$prototypesPath = $this->prototypesPath;
// fallback
if (empty($prototypesPath)) {
$prototypesPath = dirname(__DIR__) . '/prototypes';
}
if (!is_dir($prototypesPath) || !is_readable($prototypesPath)) {
throw new IllegalStateException('Unable to resolve internal prototypes directory.');
}
return $prototypesPath;
}
private function toUnixPaths(array $paths): array
{
return array_map(
static fn(string $path): string => str_replace(DIRECTORY_SEPARATOR, '/', $path),
$paths
);
}
}
Function Calls
None |
Stats
MD5 | b9ab86d31108f0e065a02901fb65dd3d |
Eval Count | 0 |
Decode Time | 136 ms |