diff --git a/config/pkg/lib/imap.yml b/config/pkg/lib/imap.yml new file mode 100644 index 00000000..cb2ec410 --- /dev/null +++ b/config/pkg/lib/imap.yml @@ -0,0 +1,14 @@ +imap: + type: library + artifact: + source: { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master" + } + metadata: + license-files: [LICENSE] + static-libs@unix: + - libc-client.a + suggests@unix: + - openssl diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 69e6c882..607d78ee 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -6,10 +6,15 @@ namespace Package\Library; use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; +use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\SourcePatcher; #[Library('imap')] class imap @@ -22,4 +27,91 @@ class imap FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"'); } } + + #[PatchBeforeBuild] + #[PatchDescription('Patch imap build system for Linux and macOS compatibility')] + public function patchBeforeBuild(LibraryPackage $lib): void + { + if (SystemTarget::getTargetOS() === 'Linux') { + $cc = getenv('CC') ?: 'gcc'; + // FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', '-DMAC_OSX_KLUDGE=1', ''); + FileSystem::replaceFileStr("{$lib->getSourceDir()}/src/osdep/unix/Makefile", 'CC=cc', "CC={$cc}"); + /* FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', '-lcrypto -lz', '-lcrypto'); + FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', '-lcrypto', '-lcrypto -lz'); + FileSystem::replaceFileStr( + $lib->getSourceDir() . '/src/osdep/unix/ssl_unix.c', + "#include \n#include ", + "#include \n#include " + ); + // SourcePatcher::patchFile('1006_openssl1.1_autoverify.patch', $lib->getSourceDir()); + SourcePatcher::patchFile('2014_openssl1.1.1_sni.patch', $lib->getSourceDir()); */ + FileSystem::replaceFileStr("{$lib->getSourceDir()}/Makefile", 'SSLINCLUDE=/usr/include/openssl', "SSLINCLUDE={$lib->getIncludeDir()}"); + FileSystem::replaceFileStr("{$lib->getSourceDir()}/Makefile", 'SSLLIB=/usr/lib', "SSLLIB={$lib->getLibDir()}"); + } elseif (SystemTarget::getTargetOS() === 'Darwin') { + $cc = getenv('CC') ?: 'clang'; + SourcePatcher::patchFile('0001_imap_macos.patch', $lib->getSourceDir()); + FileSystem::replaceFileStr($lib->getSourceDir() . '/src/osdep/unix/Makefile', 'CC=cc', "CC={$cc}"); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'SSLINCLUDE=/usr/include/openssl', 'SSLINCLUDE=' . $lib->getIncludeDir()); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'SSLLIB=/usr/lib', 'SSLLIB=' . $lib->getLibDir()); + } + } + + #[BuildFor('Linux')] + public function buildLinux(LibraryPackage $lib, PackageInstaller $installer): void + { + if ($installer->isPackageResolved('openssl')) { + $ssl_options = "SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE={$lib->getIncludeDir()} SSLLIB={$lib->getLibDir()}"; + } else { + $ssl_options = 'SSLTYPE=none'; + } + $libcVer = SystemTarget::getLibcVersion(); + $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + try { + shell() + ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.c {$lib->getLibDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.h {$lib->getIncludeDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/src/osdep/unix/*.h {$lib->getIncludeDir()}/"); + } catch (\Throwable) { + // last command throws an exception, no idea why since it works + } + } + + #[BuildFor('Darwin')] + public function buildDarwin(LibraryPackage $lib, PackageInstaller $installer): void + { + if ($installer->isPackageResolved('openssl')) { + $ssl_options = "SPECIALAUTHENTICATORS=ssl SSLTYPE=unix.nopwd SSLINCLUDE={$lib->getIncludeDir()} SSLLIB={$lib->getLibDir()}"; + } else { + $ssl_options = 'SSLTYPE=none'; + } + $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec( + "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" + ); + try { + shell() + ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.c {$lib->getLibDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/c-client/*.h {$lib->getIncludeDir()}/") + ->exec("cp -rf {$lib->getSourceDir()}/src/osdep/unix/*.h {$lib->getIncludeDir()}/"); + } catch (\Throwable) { + // last command throws an exception, no idea why since it works + } + } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index a01d29fc..80ea1aef 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -13,6 +13,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\BuildRootTracker; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; @@ -42,6 +43,9 @@ class PackageInstaller /** @var bool Whether to download missing sources automatically */ protected bool $download = true; + /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ + protected ?BuildRootTracker $tracker = null; + public function __construct(protected array $options = []) { ApplicationContext::set(PackageInstaller::class, $this); @@ -53,6 +57,11 @@ class PackageInstaller if (!empty($options['no-download'])) { $this->download = false; } + + // Initialize BuildRootTracker if tracking is enabled (default: enabled unless --no-tracker) + if (empty($options['no-tracker'])) { + $this->tracker = new BuildRootTracker(); + } } /** @@ -111,6 +120,16 @@ class PackageInstaller return $this; } + /** + * Get the BuildRootTracker instance. + * + * @return null|BuildRootTracker The tracker instance or null if tracking is disabled + */ + public function getTracker(): ?BuildRootTracker + { + return $this->tracker; + } + public function printBuildPackageOutputs(): void { foreach ($this->build_packages as $package) { @@ -183,8 +202,14 @@ class PackageInstaller InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { + // Start tracking for binary installation + $this->tracker?->startTracking($package, 'install'); $status = $this->installBinary($package); + // Stop tracking and record changes + $this->tracker?->stopTracking(); } catch (\Throwable $e) { + // Stop tracking on error + $this->tracker?->stopTracking(); if ($interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; @@ -199,6 +224,9 @@ class PackageInstaller InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { + // Start tracking for build + $this->tracker?->startTracking($package, 'build'); + if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { $dirdiff = new DirDiff(BUILD_ROOT_PATH, false); ApplicationContext::set(DirDiff::class, $dirdiff); @@ -209,7 +237,12 @@ class PackageInstaller if ($is_to_build && ($this->options['pack-mode'] ?? false) === true) { $package->runStage('packPrebuilt'); } + + // Stop tracking and record changes + $this->tracker?->stopTracking(); } catch (\Throwable $e) { + // Stop tracking on error + $this->tracker?->stopTracking(); if ($interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; diff --git a/src/StaticPHP/Util/BuildRootTracker.php b/src/StaticPHP/Util/BuildRootTracker.php new file mode 100644 index 00000000..306bf90c --- /dev/null +++ b/src/StaticPHP/Util/BuildRootTracker.php @@ -0,0 +1,189 @@ +}> Tracking data */ + protected array $tracking_data = []; + + protected static string $tracker_file = BUILD_ROOT_PATH . '/.spc-tracker.json'; + + protected ?DirDiff $current_diff = null; + + protected ?string $current_package = null; + + protected ?string $current_type = null; + + public function __construct() + { + $this->loadTrackingData(); + } + + /** + * Start tracking for a package. + * + * @param Package $package The package to track + * @param string $type The operation type: 'build' or 'install' + */ + public function startTracking(Package $package, string $type = 'build'): void + { + $this->current_package = $package->getName(); + $this->current_type = $type; + $this->current_diff = new DirDiff(BUILD_ROOT_PATH, false); + } + + /** + * Stop tracking and record the changes. + */ + public function stopTracking(): void + { + if ($this->current_diff === null || $this->current_package === null) { + return; + } + + $increment_files = $this->current_diff->getIncrementFiles(true); + + if ($increment_files !== []) { + // Remove buildroot prefix if exists and normalize paths + $normalized_files = array_map(function ($file) { + // Remove leading slashes + return ltrim($file, '/\\'); + }, $increment_files); + + $this->tracking_data[$this->current_package] = [ + 'package' => $this->current_package, + 'type' => $this->current_type, + 'files' => array_values($normalized_files), + 'time' => date('Y-m-d H:i:s'), + ]; + + $this->saveTrackingData(); + } + + $this->current_diff = null; + $this->current_package = null; + $this->current_type = null; + } + + /** + * Get tracking data for a specific package. + * + * @param string $package_name Package name + * @return null|array Tracking data or null if not found + */ + public function getPackageTracking(string $package_name): ?array + { + return $this->tracking_data[$package_name] ?? null; + } + + /** + * Get all tracking data. + * + * @return array All tracking data + */ + public function getAllTracking(): array + { + return $this->tracking_data; + } + + /** + * Find which package introduced a specific file. + * + * @param string $file File path (relative to buildroot) + * @return null|string Package name or null if not found + */ + public function findFileSource(string $file): ?string + { + $file = ltrim($file, '/\\'); + foreach ($this->tracking_data as $package_name => $data) { + if (in_array($file, $data['files'], true)) { + return $package_name; + } + } + return null; + } + + /** + * Clear tracking data for a specific package. + * + * @param string $package_name Package name + */ + public function clearPackageTracking(string $package_name): void + { + unset($this->tracking_data[$package_name]); + $this->saveTrackingData(); + } + + /** + * Clear all tracking data. + */ + public function clearAllTracking(): void + { + $this->tracking_data = []; + $this->saveTrackingData(); + } + + /** + * Get tracking statistics. + * + * @return array{total_packages: int, total_files: int, by_type: array} + */ + public function getStatistics(): array + { + $total_files = 0; + $by_type = []; + + foreach ($this->tracking_data as $data) { + $total_files += count($data['files']); + $type = $data['type']; + $by_type[$type] = ($by_type[$type] ?? 0) + 1; + } + + return [ + 'total_packages' => count($this->tracking_data), + 'total_files' => $total_files, + 'by_type' => $by_type, + ]; + } + + /** + * Get the tracker file path. + */ + public static function getTrackerFilePath(): string + { + return self::$tracker_file; + } + + /** + * Load tracking data from file. + */ + protected function loadTrackingData(): void + { + if (is_file(self::$tracker_file)) { + $content = file_get_contents(self::$tracker_file); + $data = json_decode($content, true); + if (is_array($data)) { + $this->tracking_data = $data; + } + } + } + + /** + * Save tracking data to file. + */ + protected function saveTrackingData(): void + { + FileSystem::createDir(dirname(self::$tracker_file)); + $content = json_encode($this->tracking_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + FileSystem::writeFile(self::$tracker_file, $content); + } +}