This commit is contained in:
crazywhalecc
2025-11-30 15:35:04 +08:00
parent f6c818d3c0
commit 14bfb4198a
179 changed files with 19502 additions and 655 deletions

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Config;
use StaticPHP\Exception\WrongUsageException;
class ArtifactConfig
{
private static array $artifact_configs = [];
public static function loadFromDir(string $dir): void
{
if (!is_dir($dir)) {
throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config.");
}
$files = glob("{$dir}/artifact.*.json");
if (is_array($files)) {
foreach ($files as $file) {
self::loadFromFile($file);
}
}
if (file_exists("{$dir}/artifact.json")) {
self::loadFromFile("{$dir}/artifact.json");
}
}
/**
* Load artifact configurations from a specified JSON file.
*/
public static function loadFromFile(string $file): void
{
$content = file_get_contents($file);
if ($content === false) {
throw new WrongUsageException("Failed to read artifact config file: {$file}");
}
$data = json_decode($content, true);
if (!is_array($data)) {
throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}");
}
ConfigValidator::validateAndLintArtifacts(basename($file), $data);
foreach ($data as $artifact_name => $config) {
self::$artifact_configs[$artifact_name] = $config;
}
}
/**
* Get all loaded artifact configurations.
*
* @return array<string, array> an associative array of artifact configurations
*/
public static function getAll(): array
{
return self::$artifact_configs;
}
/**
* Get the configuration for a specific artifact by name.
*
* @param string $artifact_name the name of the artifact
* @return null|array the configuration array for the specified artifact, or null if not found
*/
public static function get(string $artifact_name): ?array
{
return self::$artifact_configs[$artifact_name] ?? null;
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Config;
enum ConfigType
{
public const string LIST_ARRAY = 'list_array';
public const string ASSOC_ARRAY = 'assoc_array';
public const string STRING = 'string';
public const string BOOL = 'bool';
public const array PACKAGE_TYPES = [
'library',
'php-extension',
'target',
'virtual-target',
];
public static function validateLicenseField(mixed $value): bool
{
if (is_list_array($value)) {
foreach ($value as $item) {
if (!self::validateLicenseField($item)) {
return false;
}
}
return true;
}
if (!is_assoc_array($value)) {
return false;
}
if (!isset($value['type'])) {
return false;
}
return match ($value['type']) {
'file' => match (true) {
!isset($value['path']), !is_string($value['path']) && !is_array($value['path']) => false,
default => true,
},
'text' => match (true) {
!isset($value['text']), !is_string($value['text']) => false,
default => true,
},
default => false,
};
}
}

View File

@@ -0,0 +1,370 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Config;
use StaticPHP\Exception\ValidationException;
class ConfigValidator
{
/**
* Global field type definitions
* Maps field names to their expected types and validation rules
* Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json)
*/
public const array PACKAGE_FIELD_TYPES = [
// package fields
'type' => ConfigType::STRING,
'depends' => ConfigType::LIST_ARRAY, // @
'suggests' => ConfigType::LIST_ARRAY, // @
'artifact' => ConfigType::STRING,
'license' => [ConfigType::class, 'validateLicenseField'],
'lang' => ConfigType::STRING,
'frameworks' => ConfigType::LIST_ARRAY, // @
// php-extension type fields
'php-extension' => ConfigType::ASSOC_ARRAY,
'zend-extension' => ConfigType::BOOL,
'support' => ConfigType::ASSOC_ARRAY,
'arg-type' => ConfigType::STRING,
'build-shared' => ConfigType::BOOL,
'build-static' => ConfigType::BOOL,
'build-with-php' => ConfigType::BOOL,
'notes' => ConfigType::BOOL,
// library and target fields
'headers' => ConfigType::LIST_ARRAY, // @
'static-libs' => ConfigType::LIST_ARRAY, // @
'pkg-configs' => ConfigType::LIST_ARRAY,
'static-bins' => ConfigType::LIST_ARRAY, // @
];
public const array PACKAGE_FIELDS = [
'type' => true,
'depends' => false, // @
'suggests' => false, // @
'artifact' => false,
'license' => false,
'lang' => false,
'frameworks' => false, // @
// php-extension type fields
'php-extension' => false,
// library and target fields
'headers' => false, // @
'static-libs' => false, // @
'pkg-configs' => false,
'static-bins' => false, // @
];
public const array SUFFIX_ALLOWED_FIELDS = [
'depends',
'suggests',
'headers',
'static-libs',
'static-bins',
];
public const array PHP_EXTENSION_FIELDS = [
'zend-extension' => false,
'support' => false,
'arg-type' => false, // @
'build-shared' => false,
'build-static' => false,
'build-with-php' => false,
'notes' => false,
];
public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields]
'filelist' => [['url', 'regex'], ['extract']],
'git' => [['url', 'rev'], ['extract', 'submodules']],
'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']],
'url' => [['url'], ['filename', 'extract', 'version']],
'bitbuckettag' => [['repo'], ['extract']],
'local' => [['dirname'], ['extract']],
'pie' => [['repo'], ['extract']],
'php-release' => [[], ['extract']],
'custom' => [[], ['func']],
];
/**
* Validate and standardize artifacts configuration data.
*
* @param string $config_file_name Name of the configuration file (for error messages)
* @param mixed $data The configuration data to validate
*/
public static function validateAndLintArtifacts(string $config_file_name, mixed &$data): void
{
if (!is_array($data)) {
throw new ValidationException("{$config_file_name} is broken");
}
foreach ($data as $name => $artifact) {
foreach ($artifact as $k => $v) {
// check source field
if ($k === 'source' || $k === 'source-mirror') {
// source === custom is allowed
if ($v === 'custom') {
continue;
}
// expand string to url type (start with http:// or https://)
if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) {
$data[$name][$k] = [
'type' => 'url',
'url' => $v,
];
continue;
}
// source: object with type field
if (is_assoc_array($v)) {
self::validateArtifactObjectField($name, $v);
}
continue;
}
// check binary field
if ($k === 'binary') {
// binary === custom is allowed
if ($v === 'custom') {
$data[$name][$k] = [
'linux-x86_64' => ['type' => 'custom'],
'linux-aarch64' => ['type' => 'custom'],
'windows-x86_64' => ['type' => 'custom'],
'macos-x86_64' => ['type' => 'custom'],
'macos-aarch64' => ['type' => 'custom'],
];
continue;
}
// TODO: expand hosted to static-php hosted download urls
if ($v === 'hosted') {
continue;
}
if (is_assoc_array($v)) {
foreach ($v as $platform => $v_obj) {
self::validatePlatformString($platform);
// expand string to url type (start with http:// or https://)
if (is_string($v_obj) && (str_starts_with($v_obj, 'http://') || str_starts_with($v_obj, 'https://'))) {
$data[$name][$k][$platform] = [
'type' => 'url',
'url' => $v_obj,
];
continue;
}
// binary: object with type field
if (is_assoc_array($v_obj)) {
self::validateArtifactObjectField("{$name}::{$platform}", $v_obj);
}
}
}
}
}
}
}
/**
* Validate packages configuration data.
*
* @param string $config_file_name Name of the configuration file (for error messages)
* @param mixed $data The configuration data to validate
*/
public static function validateAndLintPackages(string $config_file_name, mixed &$data): void
{
if (!is_array($data)) {
throw new ValidationException("{$config_file_name} is broken");
}
foreach ($data as $name => $pkg) {
if (!is_assoc_array($pkg)) {
throw new ValidationException("Package [{$name}] in {$config_file_name} is not a valid associative array");
}
// check if package has valid type
if (!isset($pkg['type']) || !in_array($pkg['type'], ConfigType::PACKAGE_TYPES)) {
throw new ValidationException("Package [{$name}] in {$config_file_name} has invalid or missing 'type' field");
}
// validate basic fields using unified method
self::validatePackageFields($name, $pkg);
// validate list of suffix-allowed fields
$suffixes = ['', '@windows', '@unix', '@macos', '@linux'];
$fields = self::SUFFIX_ALLOWED_FIELDS;
self::validateSuffixAllowedFields($name, $pkg, $fields, $suffixes);
// check if "library|target" package has artifact field for target and library types
if (in_array($pkg['type'], ['target', 'library']) && !isset($pkg['artifact'])) {
throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field");
}
// check if "php-extension" package has php-extension specific fields and validate inside
if ($pkg['type'] === 'php-extension') {
self::validatePhpExtensionFields($name, $pkg);
}
// check for unknown fields
self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES));
}
}
/**
* Validate platform string format.
*
* @param string $platform Platform string, like windows-x86_64
*/
public static function validatePlatformString(string $platform): void
{
$valid_platforms = ['windows', 'linux', 'macos'];
$valid_arch = ['x86_64', 'aarch64'];
$parts = explode('-', $platform);
if (count($parts) !== 2) {
throw new ValidationException("Invalid platform format '{$platform}', expected format 'os-arch'");
}
[$os, $arch] = $parts;
if (!in_array($os, $valid_platforms)) {
throw new ValidationException("Invalid platform OS '{$os}' in platform '{$platform}'");
}
if (!in_array($arch, $valid_arch)) {
throw new ValidationException("Invalid platform architecture '{$arch}' in platform '{$platform}'");
}
}
/**
* Validate an artifact download object field.
*
* @param string $item_name Artifact name (for error messages)
* @param array $data Artifact source object data
*/
private static function validateArtifactObjectField(string $item_name, array $data): void
{
if (!isset($data['type']) || !is_string($data['type'])) {
throw new ValidationException("Artifact source object must have a valid 'type' field");
}
$type = $data['type'];
if (!isset(self::ARTIFACT_TYPE_FIELDS[$type])) {
throw new ValidationException("Artifact source object has unknown type '{$type}'");
}
[$required_fields, $optional_fields] = self::ARTIFACT_TYPE_FIELDS[$type];
// check required fields
foreach ($required_fields as $field) {
if (!isset($data[$field])) {
throw new ValidationException("Artifact source object of type '{$type}' must have required field '{$field}'");
}
}
// check for unknown fields
$allowed_fields = array_merge(['type'], $required_fields, $optional_fields);
self::validateNoInvalidFields('artifact object', $item_name, $data, $allowed_fields);
}
/**
* Unified method to validate config fields based on field definitions
*
* @param string $package_name Package name
* @param mixed $pkg The package configuration array
*/
private static function validatePackageFields(string $package_name, mixed $pkg): void
{
foreach (self::PACKAGE_FIELDS as $field => $required) {
if ($required && !isset($pkg[$field])) {
throw new ValidationException("Package {$package_name} must have [{$field}] field");
}
if (isset($pkg[$field])) {
self::validatePackageFieldType($field, $pkg[$field], $package_name);
}
}
}
/**
* Validate a field based on its global type definition
*
* @param string $field Field name
* @param mixed $value Field value
* @param string $package_name Package name (for error messages)
*/
private static function validatePackageFieldType(string $field, mixed $value, string $package_name): void
{
// Check if field exists in FIELD_TYPES
if (!isset(self::PACKAGE_FIELD_TYPES[$field])) {
// Try to strip suffix and check base field name
$suffixes = ['@windows', '@unix', '@macos', '@linux'];
$base_field = $field;
foreach ($suffixes as $suffix) {
if (str_ends_with($field, $suffix)) {
$base_field = substr($field, 0, -strlen($suffix));
break;
}
}
if (!isset(self::PACKAGE_FIELD_TYPES[$base_field])) {
// Unknown field is not allowed - strict validation
throw new ValidationException("Package {$package_name} has unknown field [{$field}]");
}
// Use base field type for validation
$expected_type = self::PACKAGE_FIELD_TYPES[$base_field];
} else {
$expected_type = self::PACKAGE_FIELD_TYPES[$field];
}
match ($expected_type) {
ConfigType::STRING => is_string($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be string"),
ConfigType::BOOL => is_bool($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be boolean"),
ConfigType::LIST_ARRAY => is_list_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be a list"),
ConfigType::ASSOC_ARRAY => is_assoc_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be an object"),
default => $expected_type($value) ?: throw new ValidationException("Package {$package_name} [{$field}] has invalid type specification"),
};
}
/**
* Validate that fields with suffixes are list arrays
*/
private static function validateSuffixAllowedFields(int|string $name, mixed $item, array $fields, array $suffixes): void
{
foreach ($fields as $field) {
foreach ($suffixes as $suffix) {
$key = $field . $suffix;
if (isset($item[$key])) {
self::validatePackageFieldType($key, $item[$key], $name);
}
}
}
}
/**
* Validate php-extension specific fields for php-extension package
*/
private static function validatePhpExtensionFields(int|string $name, mixed $pkg): void
{
if (!isset($pkg['php-extension'])) {
return;
}
if (!is_assoc_array($pkg['php-extension'])) {
throw new ValidationException("Package {$name} [php-extension] must be an object");
}
foreach (self::PHP_EXTENSION_FIELDS as $field => $required) {
if (isset($pkg['php-extension'][$field])) {
self::validatePackageFieldType($field, $pkg['php-extension'][$field], $name);
}
}
// check for unknown fields in php-extension
self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS));
}
private static function validateNoInvalidFields(string $config_type, int|string $item_name, mixed $item_content, array $allowed_fields): void
{
foreach ($item_content as $k => $v) {
// remove suffixes for checking
$base_k = $k;
$suffixes = ['@windows', '@unix', '@macos', '@linux'];
foreach ($suffixes as $suffix) {
if (str_ends_with($k, $suffix)) {
$base_k = substr($k, 0, -strlen($suffix));
break;
}
}
if (!in_array($base_k, $allowed_fields)) {
throw new ValidationException("{$config_type} [{$item_name}] has invalid field [{$base_k}]");
}
}
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Config;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
class PackageConfig
{
private static array $package_configs = [];
/**
* Load package configurations from a specified directory.
* It will look for files matching the pattern 'pkg.*.json' and 'pkg.json'.
*/
public static function loadFromDir(string $dir): void
{
if (!is_dir($dir)) {
throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config.");
}
$files = glob("{$dir}/pkg.*.json");
if (is_array($files)) {
foreach ($files as $file) {
self::loadFromFile($file);
}
}
if (file_exists("{$dir}/pkg.json")) {
self::loadFromFile("{$dir}/pkg.json");
}
}
/**
* Load package configurations from a specified JSON file.
*
* @param string $file the path to the json package configuration file
*/
public static function loadFromFile(string $file): void
{
$content = file_get_contents($file);
if ($content === false) {
throw new WrongUsageException("Failed to read package config file: {$file}");
}
$data = json_decode($content, true);
if (!is_array($data)) {
throw new WrongUsageException("Invalid JSON format in package config file: {$file}");
}
ConfigValidator::validateAndLintPackages(basename($file), $data);
foreach ($data as $pkg_name => $config) {
self::$package_configs[$pkg_name] = $config;
}
}
/**
* Check if a package configuration exists.
*/
public static function isPackageExists(string $pkg_name): bool
{
return isset(self::$package_configs[$pkg_name]);
}
public static function getAll(): array
{
return self::$package_configs;
}
/**
* Get a specific field from a package configuration.
*
* @param string $pkg_name Package name
* @param null|string $field_name Package config field name
* @param null|mixed $default Default value if field not found
* @return mixed The value of the specified field or the default value
*/
public static function get(string $pkg_name, ?string $field_name = null, mixed $default = null): mixed
{
if (!self::isPackageExists($pkg_name)) {
return $default;
}
// use suffixes to find field
$suffixes = match (SystemTarget::getTargetOS()) {
'Windows' => ['@windows', ''],
'Darwin' => ['@macos', '@unix', ''],
'Linux' => ['@linux', '@unix', ''],
'BSD' => ['@freebsd', '@bsd', '@unix', ''],
};
if ($field_name === null) {
return self::$package_configs[$pkg_name];
}
if (in_array($field_name, ConfigValidator::SUFFIX_ALLOWED_FIELDS)) {
foreach ($suffixes as $suffix) {
$suffixed_field = $field_name . $suffix;
if (isset(self::$package_configs[$pkg_name][$suffixed_field])) {
return self::$package_configs[$pkg_name][$suffixed_field];
}
}
return $default;
}
return self::$package_configs[$pkg_name][$field_name] ?? $default;
}
}