diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index fd7ef2a4f..152dc2e5c 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; -use StaticPHP\Exception\ValidationException; +use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\System\LinuxUtil; @@ -28,29 +28,41 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $arch = match (explode('-', $name)[1]) { 'x86_64' => 'amd64', 'aarch64' => 'arm64', - default => throw new ValidationException('Unsupported architecture: ' . $name), + default => throw new DownloaderException('Unsupported architecture: ' . $name), }; $os = match (explode('-', $name)[0]) { 'linux' => 'linux', 'macos' => 'darwin', - default => throw new ValidationException('Unsupported OS: ' . $name), + default => throw new DownloaderException('Unsupported OS: ' . $name), }; - $hash = match ("{$os}-{$arch}") { - 'linux-amd64' => '2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613', - 'linux-arm64' => '05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae', - 'darwin-amd64' => '5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef', - 'darwin-arm64' => '544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c', - }; - $go_version = '1.25.0'; - $url = "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz"; - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "go{$go_version}.{$os}-{$arch}.tar.gz"; + + // get version and hash + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + $page = default_shell()->executeCurl('https://go.dev/dl/'); + if ($page === '' || $page === false) { + throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); + } + + $version_regex = str_replace('.', '\.', $version); + $pattern = "/href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?([a-f0-9]{64})<\\/tt>/s"; + if (preg_match($pattern, $page, $matches)) { + $hash = $matches[1]; + } else { + throw new DownloaderException("Failed to find download hash for Go {$version} {$os}-{$arch}"); + } + + $url = "https://go.dev/dl/{$version}.{$os}-{$arch}.tar.gz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.{$os}-{$arch}.tar.gz"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); // verify hash $file_hash = hash_file('sha256', $path); if ($file_hash !== $hash) { - throw new ValidationException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); + throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $go_version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $go_version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } #[AfterBinaryExtract('go-xcaddy', [ diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b28c11dc0..74097325e 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -12,6 +12,7 @@ use StaticPHP\Artifact\Downloader\Type\Git; use StaticPHP\Artifact\Downloader\Type\GitHubRelease; use StaticPHP\Artifact\Downloader\Type\GitHubTarball; +use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; use StaticPHP\Artifact\Downloader\Type\PhpRelease; use StaticPHP\Artifact\Downloader\Type\PIE; @@ -35,6 +36,21 @@ */ class ArtifactDownloader { + /** @var array> */ + public const array DOWNLOADERS = [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array Artifact objects */ protected array $artifacts = []; @@ -355,18 +371,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, foreach ($queue as $item) { try { $instance = null; - $call = match ($item['config']['type']) { - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar', 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - default => null, - }; + $call = self::DOWNLOADERS[$item['config']['type']] ?? null; $type_display_name = match (true) { $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 2e8a499e3..731e8297e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -21,6 +21,26 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface private ?string $version = null; + public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true): array + { + logger()->debug("Fetching {$name} GitHub releases from {$repo}"); + $url = str_replace('{repo}', $repo, self::API_URL); + $headers = $this->getGitHubTokenHeaders(); + $data2 = default_shell()->executeCurl($url, headers: $headers); + $data = json_decode($data2 ?: '', true); + if (!is_array($data)) { + throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); + } + $releases = []; + foreach ($data as $release) { + if ($prefer_stable && $release['prerelease'] === true) { + continue; + } + $releases[] = $release; + } + return $releases; + } + /** * Get the latest GitHub release assets for a given repository. * match_asset is provided, only return the asset that matches the regex. diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index d773bde73..90c425075 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -7,6 +7,11 @@ trait GitHubTokenSetupTrait { public function getGitHubTokenHeaders(): array + { + return self::getGitHubTokenHeadersStatic(); + } + + public static function getGitHubTokenHeadersStatic(): array { // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php new file mode 100644 index 000000000..c5cbb3b50 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -0,0 +1,63 @@ + '{name}-{arch}-{os}-{libc}-{libcver}.txz', + 'darwin' => '{name}-{arch}-{os}.txz', + 'windows' => '{name}-{arch}-{os}.tgz', + ]; + + private static array $release_info = []; + + public static function getReleaseInfo(): array + { + if (empty(self::$release_info)) { + $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + if (empty($rel)) { + throw new DownloaderException('No releases found for hosted package-bin'); + } + self::$release_info = $rel[0]; + } + return self::$release_info; + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + $info = self::getReleaseInfo(); + $replace = [ + '{name}' => $name, + '{arch}' => SystemTarget::getTargetArch(), + '{os}' => strtolower(SystemTarget::getTargetOS()), + '{libc}' => SystemTarget::getLibc() ?? 'default', + '{libcver}' => SystemTarget::getLibcVersion() ?? 'default', + ]; + $find_str = str_replace(array_keys($replace), array_values($replace), self::ASSET_MATCHES[strtolower(SystemTarget::getTargetOS())]); + foreach ($info['assets'] as $asset) { + if ($asset['name'] === $find_str) { + $download_url = $asset['browser_download_url']; + $filename = $asset['name']; + $version = ltrim($info['tag_name'], 'v'); + logger()->debug("Downloading hosted package-bin {$name} version {$version} from GitHub: {$download_url}"); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + $headers = $this->getGitHubTokenHeaders(); + default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + } + } + throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); + } +} diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index ab8e00a8e..92b80be17 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -27,6 +27,8 @@ public function configure(): void $this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"'); $this->addOption('without-suggests', null, null, 'Do not fetch suggested sources when using --for-extensions'); + $this->addOption('without-suggestions', null, null, '(deprecated) Do not fetch suggested sources when using --for-extensions'); + // download command specific options $this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); $this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Fetch by packages, e.g "php,libssl,libcurl"'); diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php index 0a242c608..f8afd0e44 100644 --- a/src/StaticPHP/Command/SPCConfigCommand.php +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -33,7 +33,7 @@ public function handle(): int { // transform string to array $libraries = parse_comma_list($this->getOption('with-libs')); - $libraries = array_merge($libraries, $this->getOption('with-packages')); + $libraries = array_merge($libraries, parse_comma_list($this->getOption('with-packages'))); // transform string to array $extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : []; $include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts'); diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 3ddb9bab0..4de0529f0 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -137,8 +137,14 @@ public static function validateAndLintArtifacts(string $config_file_name, mixed ]; continue; } - // TODO: expand hosted to static-php hosted download urls if ($v === 'hosted') { + $data[$name][$k] = [ + 'linux-x86_64' => ['type' => 'hosted'], + 'linux-aarch64' => ['type' => 'hosted'], + 'windows-x86_64' => ['type' => 'hosted'], + 'macos-x86_64' => ['type' => 'hosted'], + 'macos-aarch64' => ['type' => 'hosted'], + ]; continue; } if (is_assoc_array($v)) { diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index d1e43de23..46d15a1ed 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -370,17 +370,6 @@ public static function resetDir(string $dir_name): void self::createDir($dir_name); } - /** - * Add source extraction hook - * - * @param string $name Source name - * @param callable $callback Callback function - */ - public static function addSourceExtractHook(string $name, callable $callback): void - { - self::$_extract_hook[$name][] = $callback; - } - /** * Check if path is relative * diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index b3e073c99..317258d03 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -6,6 +6,8 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Runtime\SystemTarget; class SPCConfigUtil @@ -99,26 +101,21 @@ public function config(array $packages = [], bool $include_suggests = false): ar * [Helper function] * Get configuration for a specific extension(s) dependencies. * - * @param Extension|Extension[] $extension Extension instance or list - * @param bool $include_suggest_ext Whether to include suggested extensions - * @param bool $include_suggest_lib Whether to include suggested libraries + * @param array|PhpExtensionPackage $extension_packages Extension instance or list * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array { - if (!is_array($extension)) { - $extension = [$extension]; + if (!is_array($extension_packages)) { + $extension_packages = [$extension_packages]; } - $libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension))); return $this->config( - extensions: array_map(fn ($x) => $x->getName(), $extension), - libraries: $libs, - include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false, - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + packages: array_map(fn ($y) => $y->getName(), $extension_packages), + include_suggests: $include_suggests, ); } @@ -126,15 +123,15 @@ public function getExtensionConfig(array|Extension $extension, bool $include_sug * [Helper function] * Get configuration for a specific library(s) dependencies. * - * @param LibraryBase|LibraryBase[] $lib Library instance or list - * @param bool $include_suggest_lib Whether to include suggested libraries + * @param array|LibraryPackage $lib Library instance or list + * @param bool $include_suggests Whether to include suggested libraries * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array + public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array { if (!is_array($lib)) { $lib = [$lib]; @@ -144,8 +141,8 @@ public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_l $save_libs_only_deps = $this->libs_only_deps; $this->libs_only_deps = true; $ret = $this->config( - libraries: array_map(fn ($x) => $x->getName(), $lib), - include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + packages: array_map(fn ($y) => $y->getName(), $lib), + include_suggests: $include_suggests, ); $this->no_php = $save_no_php; $this->libs_only_deps = $save_libs_only_deps;