mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-05 15:55:39 +08:00
Merge branch 'main' into php-85
# Conflicts: # src/SPC/util/PkgConfigUtil.php
This commit is contained in:
@@ -20,33 +20,48 @@ class ConfigValidator
|
||||
public static function validateSource(array $data): void
|
||||
{
|
||||
foreach ($data as $name => $src) {
|
||||
isset($src['type']) || throw new ValidationException("source {$name} must have prop: [type]");
|
||||
is_string($src['type']) || throw new ValidationException("source {$name} type prop must be string");
|
||||
in_array($src['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom']) || throw new ValidationException("source {$name} type [{$src['type']}] is invalid");
|
||||
switch ($src['type']) {
|
||||
case 'filelist':
|
||||
isset($src['url'], $src['regex']) || throw new ValidationException("source {$name} needs [url] and [regex] props");
|
||||
is_string($src['url']) && is_string($src['regex']) || throw new ValidationException("source {$name} [url] and [regex] must be string");
|
||||
break;
|
||||
case 'git':
|
||||
isset($src['url'], $src['rev']) || throw new ValidationException("source {$name} needs [url] and [rev] props");
|
||||
is_string($src['url']) && is_string($src['rev']) || throw new ValidationException("source {$name} [url] and [rev] must be string");
|
||||
is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string");
|
||||
break;
|
||||
case 'ghtagtar':
|
||||
case 'ghtar':
|
||||
isset($src['repo']) || throw new ValidationException("source {$name} needs [repo] prop");
|
||||
is_string($src['repo']) || throw new ValidationException("source {$name} [repo] must be string");
|
||||
is_string($src['path'] ?? '') || throw new ValidationException("source {$name} [path] must be string");
|
||||
break;
|
||||
case 'ghrel':
|
||||
isset($src['repo'], $src['match']) || throw new ValidationException("source {$name} needs [repo] and [match] props");
|
||||
is_string($src['repo']) && is_string($src['match']) || throw new ValidationException("source {$name} [repo] and [match] must be string");
|
||||
break;
|
||||
case 'url':
|
||||
isset($src['url']) || throw new ValidationException("source {$name} needs [url] prop");
|
||||
is_string($src['url']) || throw new ValidationException("source {$name} [url] must be string");
|
||||
break;
|
||||
// Validate basic source type configuration
|
||||
self::validateSourceTypeConfig($src, $name, 'source');
|
||||
|
||||
// Check source-specific fields
|
||||
// check if alt is valid
|
||||
if (isset($src['alt'])) {
|
||||
if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) {
|
||||
throw new ValidationException("source {$name} alt must be object or boolean");
|
||||
}
|
||||
if (is_assoc_array($src['alt'])) {
|
||||
// validate alt source recursively
|
||||
self::validateSource([$name . '_alt' => $src['alt']]);
|
||||
}
|
||||
}
|
||||
|
||||
// check if provide-pre-built is boolean
|
||||
if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) {
|
||||
throw new ValidationException("source {$name} provide-pre-built must be boolean");
|
||||
}
|
||||
|
||||
// check if prefer-stable is boolean
|
||||
if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) {
|
||||
throw new ValidationException("source {$name} prefer-stable must be boolean");
|
||||
}
|
||||
|
||||
// check if license is valid
|
||||
if (isset($src['license'])) {
|
||||
if (!is_assoc_array($src['license'])) {
|
||||
throw new ValidationException("source {$name} license must be object");
|
||||
}
|
||||
if (!isset($src['license']['type'])) {
|
||||
throw new ValidationException("source {$name} license must have type");
|
||||
}
|
||||
if (!in_array($src['license']['type'], ['file', 'text'])) {
|
||||
throw new ValidationException("source {$name} license type is invalid");
|
||||
}
|
||||
if ($src['license']['type'] === 'file' && !isset($src['license']['path'])) {
|
||||
throw new ValidationException("source {$name} license file must have path");
|
||||
}
|
||||
if ($src['license']['type'] === 'text' && !isset($src['license']['text'])) {
|
||||
throw new ValidationException("source {$name} license text must have text");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,7 +93,11 @@ class ConfigValidator
|
||||
if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) {
|
||||
throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}");
|
||||
}
|
||||
// check if [lib-depends|lib-suggests|static-libs][-windows|-unix|-macos|-linux] are valid list array
|
||||
// check if source is string
|
||||
if (isset($lib['source']) && !is_string($lib['source'])) {
|
||||
throw new ValidationException("lib {$name} source must be string");
|
||||
}
|
||||
// check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array
|
||||
$suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
|
||||
foreach ($suffixes as $suffix) {
|
||||
if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) {
|
||||
@@ -93,6 +112,12 @@ class ConfigValidator
|
||||
if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) {
|
||||
throw new ValidationException("lib {$name} pkg-configs must be a list");
|
||||
}
|
||||
if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) {
|
||||
throw new ValidationException("lib {$name} headers must be a list");
|
||||
}
|
||||
if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) {
|
||||
throw new ValidationException("lib {$name} bin must be a list");
|
||||
}
|
||||
}
|
||||
// check if frameworks is a list array
|
||||
if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) {
|
||||
@@ -106,7 +131,65 @@ class ConfigValidator
|
||||
*/
|
||||
public static function validateExts(mixed $data): void
|
||||
{
|
||||
is_array($data) || throw new ValidationException('ext.json is broken');
|
||||
if (!is_array($data)) {
|
||||
throw new ValidationException('ext.json is broken');
|
||||
}
|
||||
// check each extension
|
||||
foreach ($data as $name => $ext) {
|
||||
// check if ext is an assoc array
|
||||
if (!is_assoc_array($ext)) {
|
||||
throw new ValidationException("ext {$name} is not an object");
|
||||
}
|
||||
// check if ext has valid type
|
||||
if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) {
|
||||
throw new ValidationException("ext {$name} type is invalid");
|
||||
}
|
||||
// check if external ext has source
|
||||
if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) {
|
||||
throw new ValidationException("ext {$name} does not assign any source");
|
||||
}
|
||||
// check if source is string
|
||||
if (isset($ext['source']) && !is_string($ext['source'])) {
|
||||
throw new ValidationException("ext {$name} source must be string");
|
||||
}
|
||||
// check if support is valid
|
||||
if (isset($ext['support']) && !is_assoc_array($ext['support'])) {
|
||||
throw new ValidationException("ext {$name} support must be an object");
|
||||
}
|
||||
// check if notes is boolean
|
||||
if (isset($ext['notes']) && !is_bool($ext['notes'])) {
|
||||
throw new ValidationException("ext {$name} notes must be boolean");
|
||||
}
|
||||
// check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array
|
||||
$suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
|
||||
foreach ($suffixes as $suffix) {
|
||||
if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) {
|
||||
throw new ValidationException("ext {$name} lib-depends must be a list");
|
||||
}
|
||||
if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) {
|
||||
throw new ValidationException("ext {$name} lib-suggests must be a list");
|
||||
}
|
||||
if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) {
|
||||
throw new ValidationException("ext {$name} ext-depends must be a list");
|
||||
}
|
||||
}
|
||||
// check if arg-type is valid
|
||||
if (isset($ext['arg-type'])) {
|
||||
$valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path'];
|
||||
if (!in_array($ext['arg-type'], $valid_arg_types)) {
|
||||
throw new ValidationException("ext {$name} arg-type is invalid");
|
||||
}
|
||||
}
|
||||
// check if arg-type with suffix is valid
|
||||
foreach ($suffixes as $suffix) {
|
||||
if (isset($ext['arg-type' . $suffix])) {
|
||||
$valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path'];
|
||||
if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) {
|
||||
throw new ValidationException("ext {$name} arg-type{$suffix} is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +197,96 @@ class ConfigValidator
|
||||
*/
|
||||
public static function validatePkgs(mixed $data): void
|
||||
{
|
||||
is_array($data) || throw new ValidationException('pkg.json is broken');
|
||||
if (!is_array($data)) {
|
||||
throw new ValidationException('pkg.json is broken');
|
||||
}
|
||||
// check each package
|
||||
foreach ($data as $name => $pkg) {
|
||||
// check if pkg is an assoc array
|
||||
if (!is_assoc_array($pkg)) {
|
||||
throw new ValidationException("pkg {$name} is not an object");
|
||||
}
|
||||
|
||||
// Validate basic source type configuration (reuse from source validation)
|
||||
self::validateSourceTypeConfig($pkg, $name, 'pkg');
|
||||
|
||||
// Check pkg-specific fields
|
||||
// check if extract-files is valid
|
||||
if (isset($pkg['extract-files'])) {
|
||||
if (!is_assoc_array($pkg['extract-files'])) {
|
||||
throw new ValidationException("pkg {$name} extract-files must be an object");
|
||||
}
|
||||
// check each extract file mapping
|
||||
foreach ($pkg['extract-files'] as $source => $target) {
|
||||
if (!is_string($source) || !is_string($target)) {
|
||||
throw new ValidationException("pkg {$name} extract-files mapping must be string to string");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate pre-built.json configuration
|
||||
*
|
||||
* @param mixed $data pre-built.json loaded data
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function validatePreBuilt(mixed $data): void
|
||||
{
|
||||
if (!is_array($data)) {
|
||||
throw new ValidationException('pre-built.json is broken');
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!isset($data['repo'])) {
|
||||
throw new ValidationException('pre-built.json must have [repo] field');
|
||||
}
|
||||
if (!is_string($data['repo'])) {
|
||||
throw new ValidationException('pre-built.json [repo] must be string');
|
||||
}
|
||||
|
||||
// Check optional prefer-stable field
|
||||
if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) {
|
||||
throw new ValidationException('pre-built.json [prefer-stable] must be boolean');
|
||||
}
|
||||
|
||||
// Check match pattern fields (at least one must exist)
|
||||
$pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows'];
|
||||
$has_pattern = false;
|
||||
|
||||
foreach ($pattern_fields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$has_pattern = true;
|
||||
if (!is_string($data[$field])) {
|
||||
throw new ValidationException("pre-built.json [{$field}] must be string");
|
||||
}
|
||||
// Validate pattern contains required placeholders
|
||||
if (!str_contains($data[$field], '{name}')) {
|
||||
throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder");
|
||||
}
|
||||
if (!str_contains($data[$field], '{arch}')) {
|
||||
throw new ValidationException("pre-built.json [{$field}] must contain {arch} placeholder");
|
||||
}
|
||||
if (!str_contains($data[$field], '{os}')) {
|
||||
throw new ValidationException("pre-built.json [{$field}] must contain {os} placeholder");
|
||||
}
|
||||
|
||||
// Linux pattern should have libc-related placeholders
|
||||
if ($field === 'match-pattern-linux') {
|
||||
if (!str_contains($data[$field], '{libc}')) {
|
||||
throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libc} placeholder');
|
||||
}
|
||||
if (!str_contains($data[$field], '{libcver}')) {
|
||||
throw new ValidationException('pre-built.json [match-pattern-linux] must contain {libcver} placeholder');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_pattern) {
|
||||
throw new ValidationException('pre-built.json must have at least one match-pattern field');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,4 +414,85 @@ class ConfigValidator
|
||||
$craft['craft-options']['build'] ??= true;
|
||||
return $craft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate source type configuration (shared between source.json and pkg.json)
|
||||
*
|
||||
* @param array $item The source/package item to validate
|
||||
* @param string $name The name of the item for error messages
|
||||
* @param string $config_type The type of config file ("source" or "pkg")
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private static function validateSourceTypeConfig(array $item, string $name, string $config_type): void
|
||||
{
|
||||
if (!isset($item['type'])) {
|
||||
throw new ValidationException("{$config_type} {$name} must have prop: [type]");
|
||||
}
|
||||
if (!is_string($item['type'])) {
|
||||
throw new ValidationException("{$config_type} {$name} type prop must be string");
|
||||
}
|
||||
if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) {
|
||||
throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid");
|
||||
}
|
||||
|
||||
// Validate type-specific requirements
|
||||
switch ($item['type']) {
|
||||
case 'filelist':
|
||||
if (!isset($item['url'], $item['regex'])) {
|
||||
throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props");
|
||||
}
|
||||
if (!is_string($item['url']) || !is_string($item['regex'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string");
|
||||
}
|
||||
break;
|
||||
case 'git':
|
||||
if (!isset($item['url'], $item['rev'])) {
|
||||
throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props");
|
||||
}
|
||||
if (!is_string($item['url']) || !is_string($item['rev'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string");
|
||||
}
|
||||
if (isset($item['path']) && !is_string($item['path'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [path] must be string");
|
||||
}
|
||||
break;
|
||||
case 'ghtagtar':
|
||||
case 'ghtar':
|
||||
if (!isset($item['repo'])) {
|
||||
throw new ValidationException("{$config_type} {$name} needs [repo] prop");
|
||||
}
|
||||
if (!is_string($item['repo'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [repo] must be string");
|
||||
}
|
||||
if (isset($item['path']) && !is_string($item['path'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [path] must be string");
|
||||
}
|
||||
break;
|
||||
case 'ghrel':
|
||||
if (!isset($item['repo'], $item['match'])) {
|
||||
throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props");
|
||||
}
|
||||
if (!is_string($item['repo']) || !is_string($item['match'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string");
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
if (!isset($item['url'])) {
|
||||
throw new ValidationException("{$config_type} {$name} needs [url] prop");
|
||||
}
|
||||
if (!is_string($item['url'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [url] must be string");
|
||||
}
|
||||
if (isset($item['filename']) && !is_string($item['filename'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [filename] must be string");
|
||||
}
|
||||
if (isset($item['path']) && !is_string($item['path'])) {
|
||||
throw new ValidationException("{$config_type} {$name} [path] must be string");
|
||||
}
|
||||
break;
|
||||
case 'custom':
|
||||
// custom type has no specific requirements
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,30 @@ use SPC\builder\Extension;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
/**
|
||||
* Custom extension attribute and manager
|
||||
*
|
||||
* This class provides functionality to register and manage custom PHP extensions
|
||||
* that can be used during the build process.
|
||||
*/
|
||||
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
|
||||
class CustomExt
|
||||
{
|
||||
private static array $custom_ext_class = [];
|
||||
|
||||
/**
|
||||
* Constructor for custom extension attribute
|
||||
*
|
||||
* @param string $ext_name The extension name
|
||||
*/
|
||||
public function __construct(protected string $ext_name) {}
|
||||
|
||||
/**
|
||||
* Load all custom extension classes
|
||||
*
|
||||
* This method scans the extension directory and registers all classes
|
||||
* that have the CustomExt attribute.
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
@@ -32,6 +46,12 @@ class CustomExt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class name for a custom extension
|
||||
*
|
||||
* @param string $ext_name The extension name
|
||||
* @return string The class name for the extension
|
||||
*/
|
||||
public static function getExtClass(string $ext_name): string
|
||||
{
|
||||
return self::$custom_ext_class[$ext_name] ?? Extension::class;
|
||||
|
||||
@@ -9,13 +9,25 @@ use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
|
||||
/**
|
||||
* Dependency processing tool class, including processing extensions, library dependency list order, etc.
|
||||
* Dependency processing tool class
|
||||
*
|
||||
* This class handles processing extensions, library dependency list ordering, etc.
|
||||
* It provides utilities for managing dependencies between extensions and libraries.
|
||||
*/
|
||||
class DependencyUtil
|
||||
{
|
||||
/**
|
||||
* Convert platform extensions to library dependencies and suggestions.
|
||||
* Convert platform extensions to library dependencies and suggestions
|
||||
*
|
||||
* This method processes all extensions and libraries to create a comprehensive
|
||||
* dependency map that can be used for build ordering.
|
||||
*
|
||||
* Returns an associative array where the key is the extension or library name (string),
|
||||
* and the value is an array with two keys:
|
||||
* - 'depends': array of dependency names (string)
|
||||
* - 'suggests': array of suggested dependency names (string)
|
||||
*
|
||||
* @return array<string, array{depends: array<int, string>, suggests: array<int, string>}>
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
@@ -53,6 +65,10 @@ class DependencyUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library dependencies in correct order
|
||||
*
|
||||
* @param array $libs Array of library names
|
||||
* @return array Ordered array of library names
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
@@ -88,7 +104,13 @@ class DependencyUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileSystemException|WrongUsageException
|
||||
* Get extension dependencies in correct order
|
||||
*
|
||||
* @param array $exts Array of extension names
|
||||
* @param array $additional_libs Array of additional libraries
|
||||
* @return array Ordered array of extension names
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function getExtsAndLibs(array $exts, array $additional_libs = [], bool $include_suggested_exts = false, bool $include_suggested_libs = false): array
|
||||
{
|
||||
@@ -155,9 +177,6 @@ class DependencyUtil
|
||||
return [$exts_final, $libs_final, $not_included_final];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
private static function doVisitPlat(array $deps, array $dep_list): array
|
||||
{
|
||||
// default: get extension exts and libs sorted by dep_list
|
||||
|
||||
@@ -6,6 +6,12 @@ namespace SPC\util;
|
||||
|
||||
use SPC\exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Utility class for pkg-config operations
|
||||
*
|
||||
* This class provides methods to interact with pkg-config to get
|
||||
* compilation flags and library information for building extensions.
|
||||
*/
|
||||
class PkgConfigUtil
|
||||
{
|
||||
/**
|
||||
@@ -24,12 +30,14 @@ class PkgConfigUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns --cflags-only-other output.
|
||||
* Get CFLAGS from pkg-config
|
||||
*
|
||||
* Returns --cflags-only-other output from pkg-config.
|
||||
* The reason we return the string is we cannot use array_unique() on cflags,
|
||||
* some cflags may contains spaces.
|
||||
*
|
||||
* @param string $pkg_config_str .pc file str, accepts multiple files
|
||||
* @return string cflags string, e.g. "-Wno-implicit-int-float-conversion ..."
|
||||
* @param string $pkg_config_str .pc file string, accepts multiple files
|
||||
* @return string CFLAGS string, e.g. "-Wno-implicit-int-float-conversion ..."
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getCflags(string $pkg_config_str): string
|
||||
@@ -40,10 +48,12 @@ class PkgConfigUtil
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library flags from pkg-config
|
||||
*
|
||||
* Returns --libs-only-l and --libs-only-other output.
|
||||
* The reason we return the array is to avoid duplicate lib defines.
|
||||
*
|
||||
* @param string $pkg_config_str .pc file str, accepts multiple files
|
||||
* @param string $pkg_config_str .pc file string, accepts multiple files
|
||||
* @return array Unique libs array, e.g. [-lz, -lxml, ...]
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
@@ -79,6 +89,13 @@ class PkgConfigUtil
|
||||
return array_reverse(array_unique(array_reverse($libs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute pkg-config command and return result
|
||||
*
|
||||
* @param string $cmd The pkg-config command to execute
|
||||
* @return string The command output
|
||||
* @throws RuntimeException If command fails
|
||||
*/
|
||||
private static function execWithResult(string $cmd): string
|
||||
{
|
||||
f_exec($cmd, $output, $result_code);
|
||||
|
||||
Reference in New Issue
Block a user