diff --git a/skeleton-test.php b/skeleton-test.php new file mode 100644 index 00000000..689d3385 --- /dev/null +++ b/skeleton-test.php @@ -0,0 +1,21 @@ +addDependency('bar') + ->addStaticLib('libfoo.a', 'unix') + ->addStaticLib('libfoo.a', 'unix') + ->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz'])); + +$pkg_config = $package_generator->generateConfig(); +$artifact_config = $artifact_generator->generateConfig(); + +echo "===== pkg.json =====" . PHP_EOL; +echo json_encode($pkg_config, 64|128|256) . PHP_EOL; +echo "===== artifact.json =====" . PHP_EOL; +echo json_encode($artifact_config, 64|128|256) . PHP_EOL; diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 2ac7b454..a7347395 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -8,6 +8,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomSource; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php index d25c6dd1..e840c0bf 100644 --- a/src/StaticPHP/Config/ArtifactConfig.php +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace StaticPHP\Config; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\Registry; class ArtifactConfig { private static array $artifact_configs = []; - public static function loadFromDir(string $dir): void + public static function loadFromDir(string $dir, string $registry_name): void { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config."); @@ -18,18 +19,18 @@ class ArtifactConfig $files = glob("{$dir}/artifact.*.json"); if (is_array($files)) { foreach ($files as $file) { - self::loadFromFile($file); + self::loadFromFile($file, $registry_name); } } if (file_exists("{$dir}/artifact.json")) { - self::loadFromFile("{$dir}/artifact.json"); + self::loadFromFile("{$dir}/artifact.json", $registry_name); } } /** * Load artifact configurations from a specified JSON file. */ - public static function loadFromFile(string $file): void + public static function loadFromFile(string $file, string $registry_name): void { $content = file_get_contents($file); if ($content === false) { @@ -42,6 +43,7 @@ class ArtifactConfig ConfigValidator::validateAndLintArtifacts(basename($file), $data); foreach ($data as $artifact_name => $config) { self::$artifact_configs[$artifact_name] = $config; + Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $file); } } diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index dc0b3d54..3342dfc7 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace StaticPHP\Config; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\Registry; use StaticPHP\Runtime\SystemTarget; class PackageConfig @@ -15,7 +16,7 @@ class PackageConfig * 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 + public static function loadFromDir(string $dir, string $registry_name): void { if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); @@ -23,11 +24,11 @@ class PackageConfig $files = glob("{$dir}/pkg.*.json"); if (is_array($files)) { foreach ($files as $file) { - self::loadFromFile($file); + self::loadFromFile($file, $registry_name); } } if (file_exists("{$dir}/pkg.json")) { - self::loadFromFile("{$dir}/pkg.json"); + self::loadFromFile("{$dir}/pkg.json", $registry_name); } } @@ -36,7 +37,7 @@ class PackageConfig * * @param string $file the path to the json package configuration file */ - public static function loadFromFile(string $file): void + public static function loadFromFile(string $file, string $registry_name): void { $content = file_get_contents($file); if ($content === false) { @@ -49,6 +50,7 @@ class PackageConfig ConfigValidator::validateAndLintPackages(basename($file), $data); foreach ($data as $pkg_name => $config) { self::$package_configs[$pkg_name] = $config; + Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file); } } diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0e5371ac..bd0cfd60 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -9,6 +9,7 @@ use StaticPHP\Command\BuildTargetCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\ShellCommand; +use StaticPHP\Command\Dev\SkeletonCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\ExtractCommand; @@ -27,12 +28,12 @@ class ConsoleApplication extends Application public function __construct() { - parent::__construct('static-php-cli', self::VERSION); + parent::__construct('StaticPHP', self::VERSION); require_once ROOT_DIR . '/src/bootstrap.php'; - // check registry - Registry::checkLoadedRegistries(); + // resolve registry + Registry::resolve(); /** * @var string $name @@ -59,6 +60,7 @@ class ConsoleApplication extends Application new ShellCommand(), new IsInstalledCommand(), new EnvCommand(), + new SkeletonCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 4ae5df4f..909de021 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -13,9 +13,13 @@ use Symfony\Component\Yaml\Yaml; class Registry { - /** @var string[] List of loaded registry names */ + /** @var array List of loaded registries */ private static array $loaded_registries = []; + /** @var array Maps of package and artifact names to their registry config file paths (for reverse lookup) */ + private static array $package_reversed_registry_files = []; + private static array $artifact_reversed_registry_files = []; + /** * Load a registry from file path. * This method handles external registries that may not be in composer autoload. @@ -85,9 +89,9 @@ class Registry foreach ($data['package']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); if (is_file($path)) { - PackageConfig::loadFromFile($path); + PackageConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - PackageConfig::loadFromDir($path); + PackageConfig::loadFromDir($path, $registry_name); } } } @@ -97,9 +101,9 @@ class Registry foreach ($data['artifact']['config'] as $path) { $path = self::fullpath($path, dirname($registry_file)); if (is_file($path)) { - ArtifactConfig::loadFromFile($path); + ArtifactConfig::loadFromFile($path, $registry_name); } elseif (is_dir($path)) { - ArtifactConfig::loadFromDir($path); + ArtifactConfig::loadFromDir($path, $registry_name); } } } @@ -187,7 +191,12 @@ class Registry } } - public static function checkLoadedRegistries(): void + /** + * Resolve loaded registries. + * This method finalizes the loading process by registering default stages + * and validating stage events. + */ + public static function resolve(): void { // Register default stages for all PhpExtensionPackage instances // This must be done after all registries are loaded to ensure custom stages take precedence @@ -217,6 +226,42 @@ class Registry self::$loaded_registries = []; } + /** + * Bind a package name to its registry config file for reverse lookup. + * + * @internal + */ + public static function _bindPackageConfigFile(string $package_name, string $registry_name, string $config_file): void + { + self::$package_reversed_registry_files[$package_name] = [ + 'registry' => $registry_name, + 'config' => $config_file, + ]; + } + + /** + * Bind an artifact name to its registry config file for reverse lookup. + * + * @internal + */ + public static function _bindArtifactConfigFile(string $artifact_name, string $registry_name, string $config_file): void + { + self::$artifact_reversed_registry_files[$artifact_name] = [ + 'registry' => $registry_name, + 'config' => $config_file, + ]; + } + + public static function getPackageConfigInfo(string $package_name): ?array + { + return self::$package_reversed_registry_files[$package_name] ?? null; + } + + public static function getArtifactConfigInfo(string $artifact_name): ?array + { + return self::$artifact_reversed_registry_files[$artifact_name] ?? null; + } + /** * Parse a class entry from the classes array. * Supports two formats: diff --git a/src/StaticPHP/Skeleton/ArtifactGenerator.php b/src/StaticPHP/Skeleton/ArtifactGenerator.php new file mode 100644 index 00000000..031d095b --- /dev/null +++ b/src/StaticPHP/Skeleton/ArtifactGenerator.php @@ -0,0 +1,57 @@ +name; + } + + public function setSource(array $source): static + { + $clone = clone $this; + $clone->source = $source; + return $clone; + } + + public function setCustomSource(): static + { + $clone = clone $this; + $clone->source = ['type' => 'custom']; + $clone->generate_class = true; + $clone->generate_custom_source_func = true; + return $clone; + } + + public function getSource(): ?array + { + return $this->source; + } + + public function generateConfig(): array + { + $config = []; + + if ($this->source) { + $config['source'] = $this->source; + } + return $config; + } +} diff --git a/src/StaticPHP/Skeleton/ExecutorGenerator.php b/src/StaticPHP/Skeleton/ExecutorGenerator.php new file mode 100644 index 00000000..02edf2eb --- /dev/null +++ b/src/StaticPHP/Skeleton/ExecutorGenerator.php @@ -0,0 +1,16 @@ + $depends An array of dependencies required by the package, categorized by operating system. */ + protected array $depends = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */ + protected array $suggests = []; + + /** @var array $frameworks An array of macOS frameworks for the package */ + protected array $frameworks = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */ + protected array $static_libs = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $headers An array of header files required by the package, categorized by operating system. */ + protected array $headers = []; + + /** @var array<''|'unix'|'windows'|'macos'|'linux', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */ + protected array $static_bins = []; + + /** @var ArtifactGenerator|null $artifact Artifact */ + protected ?ArtifactGenerator $artifact = null; + + /** @var array $licenses Licenses */ + protected array $licenses = []; + + /** @var array<'Darwin'|'Linux'|'Windows', null|string> $build_for_enables Enable build function generating */ + protected array $build_for_enables = [ + 'Darwin' => null, + 'Linux' => null, + 'Windows' => null, + ]; + + /** @var array */ + protected array $func_executor_binding = []; + + /** + * @param string $package_name Package name + * @param 'library'|'target'|'virtual-target'|'php-extension' $type Package type ('library', 'target', 'virtual-target', etc.) + */ + public function __construct(protected string $package_name, protected string $type) {} + + /** + * Add package dependency. + * + * @param string $package Package name + * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + */ + public function addDependency(string $package, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->depends[$os])) { + $clone->depends[$os] = []; + } + if (!in_array($package, $clone->depends[$os], true)) { + $clone->depends[$os][] = $package; + } + return $clone; + } + + /** + * Add package suggestion. + * + * @param string $package Package name + * @param string $os Operating system ('' for all OSes, '@unix', '@windows', '@macos') + */ + public function addSuggestion(string $package, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->suggests[$os])) { + $clone->suggests[$os] = []; + } + if (!in_array($package, $clone->suggests[$os], true)) { + $clone->suggests[$os][] = $package; + } + return $clone; + } + + public function addStaticLib(string $lib_a, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) { + throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}"); + } + if (str_ends_with($lib_a, '.lib') && in_array($os, ['unix', 'linux', 'macos'], true)) { + throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}"); + } + if (str_ends_with($lib_a, '.a') && $os === 'windows') { + throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}"); + } + if (isset($this->static_libs[$os]) && in_array($lib_a, $this->static_libs[$os], true)) { + // already exists + return $this; + } + $clone = clone $this; + if (!isset($clone->static_libs[$os])) { + $clone->static_libs[$os] = []; + } + if (!in_array($lib_a, $clone->static_libs[$os], true)) { + $clone->static_libs[$os][] = $lib_a; + } + return $clone; + } + + public function addHeader(string $header_file, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->headers[$os])) { + $clone->headers[$os] = []; + } + if (!in_array($header_file, $clone->headers[$os], true)) { + $clone->headers[$os][] = $header_file; + } + return $clone; + } + + public function addStaticBin(string $bin_file, string $os = ''): static + { + if (!in_array($os, ['', 'unix', 'windows', 'macos', 'linux'], true)) { + throw new ValidationException("Invalid OS suffix: {$os}"); + } + $clone = clone $this; + if (!isset($clone->static_bins[$os])) { + $clone->static_bins[$os] = []; + } + if (!in_array($bin_file, $clone->static_bins[$os], true)) { + $clone->static_bins[$os][] = $bin_file; + } + return $clone; + } + + /** + * Add package artifact. + * + * @param ArtifactGenerator $artifactGenerator Artifact generator + */ + public function addArtifact(ArtifactGenerator $artifactGenerator): static + { + $clone = clone $this; + $clone->artifact = $artifactGenerator; + return $clone; + } + + /** + * Add license from string. + * + * @param string $text License content + */ + public function addLicenseFromString(string $text): static + { + $clone = clone $this; + $clone->licenses[] = [ + 'type' => 'text', + 'text' => $text, + ]; + return $clone; + } + + /** + * Add license from file. + * + * @param string $file_path License file path + */ + public function addLicenseFromFile(string $file_path): static + { + $clone = clone $this; + $clone->licenses[] = [ + 'type' => 'file', + 'path' => $file_path, + ]; + return $clone; + } + + /** + * Enable build for specific OS. + * + * @param 'Windows'|'Linux'|'Darwin'|array<'Windows'|'Linux'|'Darwin'> $build_for Build for OS + */ + public function enableBuild(string|array $build_for, ?string $build_function_name = null): static + { + $clone = clone $this; + if (is_array($build_for)) { + foreach ($build_for as $bf) { + $clone = $clone->enableBuild($bf, $build_function_name ?? 'build'); + } + return $clone; + } + $clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}"; + return $clone; + } + + /** + * Bind function executor. + * + * @param string $func_name Function name + * @param ExecutorGenerator $executor Executor generator + */ + public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static + { + $clone = clone $this; + $clone->func_executor_binding[$func_name] = $executor; + return $clone; + } + + /** + * Generate package config + */ + public function generateConfig(): array + { + $config = ['type' => $this->type]; + + // Add dependencies + foreach ($this->depends as $suffix => $depends) { + $k = $suffix !== '' ? "depends@{$suffix}" : 'depends'; + $config[$k] = $depends; + } + + // add suggests + foreach ($this->suggests as $suffix => $suggests) { + $k = $suffix !== '' ? "suggests@{$suffix}" : 'suggests'; + $config[$k] = $suggests; + } + + // Add frameworks + if (!empty($this->frameworks)) { + $config['frameworks'] = $this->frameworks; + } + + // Add static libs + foreach ($this->static_libs as $suffix => $libs) { + $k = $suffix !== '' ? "static-libs@{$suffix}" : 'static-libs'; + $config[$k] = $libs; + } + + // Add headers + foreach ($this->headers as $suffix => $headers) { + $k = $suffix !== '' ? "headers@{$suffix}" : 'headers'; + $config[$k] = $headers; + } + + // Add static bins + foreach ($this->static_bins as $suffix => $bins) { + $k = $suffix !== '' ? "static-bins@{$suffix}" : 'static-bins'; + $config[$k] = $bins; + } + + // Add artifact + if ($this->artifact !== null) { + $config['artifact'] = $this->artifact->getName(); + } + + // Add licenses + if (!empty($this->licenses)) { + if (count($this->licenses) === 1) { + $config['license'] = $this->licenses[0]; + } else { + $config['license'] = $this->licenses; + } + } + + return $config; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ba917332..14397d03 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,6 +4,6 @@ declare(strict_types=1); use Psr\Log\LogLevel; require_once __DIR__ . '/../src/bootstrap.php'; -\StaticPHP\Registry\Registry::checkLoadedRegistries(); +\StaticPHP\Registry\Registry::resolve(); logger()->setLevel(LogLevel::ERROR);