From 600ba825c895e8b17d3b6272d065f8b79ed08253 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 6 Dec 2025 16:39:23 +0800 Subject: [PATCH] Enhance musl-wrapper and musl-toolchain installation process --- config/artifact.json | 15 ++++- src/SPC/builder/linux/SystemUtil.php | 2 +- src/SPC/doctor/item/LinuxToolCheckList.php | 4 +- src/StaticPHP/Artifact/ArtifactDownloader.php | 17 ++++-- src/StaticPHP/Artifact/ArtifactExtractor.php | 13 ++-- src/StaticPHP/Doctor/Doctor.php | 7 ++- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 61 ++++++++++++++----- src/StaticPHP/Util/System/LinuxUtil.php | 18 +++++- 8 files changed, 102 insertions(+), 35 deletions(-) diff --git a/config/artifact.json b/config/artifact.json index a2c6ba4fb..de30a1e4c 100644 --- a/config/artifact.json +++ b/config/artifact.json @@ -8,6 +8,9 @@ } } }, + "musl-wrapper": { + "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" + }, "php-src": { "source": { "type": "php-release" @@ -28,8 +31,16 @@ }, "musl-toolchain": { "binary": { - "linux-x86_64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", - "linux-aarch64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz" + "linux-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + }, + "linux-aarch64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz", + "extract": "{pkg_root_path}/musl-toolchain" + } } }, "pkg-config": { diff --git a/src/SPC/builder/linux/SystemUtil.php b/src/SPC/builder/linux/SystemUtil.php index 056af090d..30b36b888 100644 --- a/src/SPC/builder/linux/SystemUtil.php +++ b/src/SPC/builder/linux/SystemUtil.php @@ -141,7 +141,7 @@ public static function getSupportedDistros(): array { return [ // debian-like - 'debian', 'ubuntu', 'Deepin', + 'debian', 'ubuntu', 'Deepin', 'neon', // rhel-like 'redhat', // centos diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index ab8144a21..08a2b4dc9 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -112,7 +112,7 @@ public function checkSystemOSPackages(): ?CheckResult public function fixBuildTools(array $distro, array $missing): bool { $install_cmd = match ($distro['dist']) { - 'ubuntu', 'debian', 'Deepin' => 'apt-get install -y', + 'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y', 'alpine' => 'apk add', 'redhat' => 'dnf install -y', 'centos' => 'yum install -y', @@ -128,7 +128,7 @@ public function fixBuildTools(array $distro, array $missing): bool logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)'); } - $is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin']); + $is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']); $to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing; // debian, alpine libtool -> libtoolize $to_install = str_replace('libtoolize', 'libtool', $to_install); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b28c11dc0..29db2018c 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -304,7 +304,7 @@ public function download(bool $interactive = true): void $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } @@ -342,7 +342,7 @@ public function getOption(string $name, mixed $default = null): mixed return $this->options[$name] ?? $default; } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -374,7 +374,7 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false) { + if ($parallel === false && $interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -392,7 +392,12 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, $instance = new $call(); $lock = $instance->download($artifact->getName(), $item['config'], $this); } else { - throw new ValidationException("Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}."); + if ($item['config']['type'] === 'custom') { + $msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.'; + } else { + $msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}."; + } + throw new ValidationException($msg); } if (!$lock instanceof DownloadResult) { throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult."); @@ -408,13 +413,13 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false) { + if ($parallel === false && $interactive) { $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); } return SPC_DOWNLOAD_STATUS_SUCCESS; } catch (DownloaderException|ExecutionException $e) { - if ($parallel === false) { + if ($parallel === false && $interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index ac0e6ae2c..11b738a70 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -78,12 +78,17 @@ public function extractForPackages(array $packages, bool $force_source = false): /** * Extract a single artifact. * - * @param Artifact $artifact The artifact to extract - * @param bool $force_source If true, always extract source (ignore binary) + * @param Artifact|string $artifact The artifact to extract + * @param bool $force_source If true, always extract source (ignore binary) */ - public function extract(Artifact $artifact, bool $force_source = false): int + public function extract(Artifact|string $artifact, bool $force_source = false): int { - $name = $artifact->getName(); + if (is_string($artifact)) { + $name = $artifact; + $artifact = ArtifactLoader::getArtifactInstance($name); + } else { + $name = $artifact->getName(); + } // Already extracted in this session if (isset($this->extracted[$name])) { diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 8141fa32f..692a5b8c5 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -34,7 +34,7 @@ public function checkAll(bool $interactive = true): bool InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check)) { + if (!$this->checkItem($check, $interactive)) { return false; } } @@ -47,7 +47,7 @@ public function checkAll(bool $interactive = true): bool * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check): bool + public function checkItem(CheckItem|string $check, bool $interactive = true): bool { if (is_string($check)) { $found = null; @@ -63,7 +63,8 @@ public function checkItem(CheckItem|string $check): bool } $check = $found; } - $this->output?->write("Checking {$check->item_name} ... "); + $prepend = $interactive ? ' - ' : ''; + $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check $result = call_user_func($check->callback); diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index a48300e7b..4d7a86bee 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -4,13 +4,21 @@ namespace StaticPHP\Doctor\Item; +use StaticPHP\Artifact\ArtifactCache; +use StaticPHP\Artifact\ArtifactDownloader; +use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Attribute\Doctor\OptionalCheck; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\MuslToolchain; use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\InteractiveTerm; +use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\System\LinuxUtil; #[OptionalCheck([self::class, 'optionalCheck'])] @@ -18,8 +26,8 @@ class LinuxMuslCheck { public static function optionalCheck(): bool { - return getenv('SPC_TOOLCHAIN') === MuslToolchain::class || - (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()); + $toolchain = ApplicationContext::get(ToolchainInterface::class); + return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist(); } /** @noinspection PhpUnused */ @@ -51,23 +59,46 @@ public function checkMuslCrossMake(): CheckResult #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - // TODO: implement musl-wrapper installation - // This should: - // 1. Download musl source using Downloader::downloadSource() - // 2. Extract the source using FileSystem::extractSource() - // 3. Apply CVE patches using SourcePatcher::patchFile() - // 4. Build and install musl wrapper - // 5. Add path using putenv instead of editing /etc/profile - return false; + $downloader = new ArtifactDownloader(); + $downloader->add('musl-wrapper')->download(false); + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $extractor->extract('musl-wrapper'); + + // Apply CVE-2025-26519 patch and install musl wrapper + SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . '/musl-wrapper'); + SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0002.patch', SOURCE_PATH . '/musl-wrapper'); + + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + shell()->cd(SOURCE_PATH . '/musl-wrapper') + ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') + ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j') + ->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + return true; } #[FixItem('fix-musl-cross-make')] public function fixMuslCrossMake(): bool { - // TODO: implement musl-cross-make installation - // This should: - // 1. Install musl-toolchain package using PackageManager::installPackage() - // 2. Copy toolchain files to /usr/local/musl - return false; + // sudo + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + $downloader = new ArtifactDownloader(); + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $downloader->add('musl-toolchain')->download(false); + $extractor->extract('musl-toolchain'); + $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; + shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); + FileSystem::removeDir($pkg_root); + return true; } } diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php index 2b1d44f43..79ad89ed7 100644 --- a/src/StaticPHP/Util/System/LinuxUtil.php +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -13,13 +13,14 @@ class LinuxUtil extends UnixUtil * Get current linux distro name and version. * * @noinspection PhpMissingBreakStatementInspection - * @return array{dist: string, ver: string} Linux distro info (unknown if not found) + * @return array{dist: string, ver: string, family: string} Linux distro info (unknown if not found) */ public static function getOSRelease(): array { $ret = [ 'dist' => 'unknown', 'ver' => 'unknown', + 'family' => 'unknown', ]; switch (true) { case file_exists('/etc/centos-release'): @@ -44,6 +45,9 @@ public static function getOSRelease(): array if (preg_match('/^ID=(.*)$/', $line, $matches)) { $ret['dist'] = $matches[1]; } + if (preg_match('/^ID_LIKE=(.*)$/', $line, $matches)) { + $ret['family'] = $matches[1]; + } if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) { $ret['ver'] = $matches[1]; } @@ -91,7 +95,7 @@ public static function getSupportedDistros(): array { return [ // debian-like - 'debian', 'ubuntu', 'Deepin', + 'debian', 'ubuntu', 'Deepin', 'neon', // rhel-like 'redhat', // centos @@ -103,6 +107,16 @@ public static function getSupportedDistros(): array ]; } + /** + * Check if current linux distro is debian-based. + */ + public static function isDebianDist(): bool + { + $dist = static::getOSRelease()['dist']; + $family = explode(' ', static::getOSRelease()['family']); + return in_array($dist, ['debian', 'ubuntu', 'Deepin', 'neon']) || in_array('debian', $family); + } + /** * Get libc version string from ldd. */