From 8dfb3634224fbd596b5f6d726654415a4adea8a5 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 8 Sep 2026 17:58:44 +0800 Subject: [PATCH] feat(platform): add Android arm64-v8a cross-compilation support - Implement Android platform class with API level detection and compiler configuration - Add Android-specific library linking with proper archive grouping and system libraries - Integrate Android target detection and SDK directory resolution in build configuration - Add Android-specific compile flags including static libstdc++ and security hardening - Include Android platform support in translator and compiler base classes - Add PHPX_ANDROID=1 define flag for Android builds - Support --- .../Build/NativeBuildConfigurationTest.php | 39 +++++ phpunit/src/FixedTypeDefaultValueTest.php | 23 +++ src/Build/NativeBuildConfigurationTrait.php | 40 +++++- src/Build/NativeCommandOptionsTrait.php | 3 + src/CompilerBase.php | 9 ++ src/Platform/Android.php | 134 ++++++++++++++++++ src/Translator.php | 8 ++ .../basic/unset-fixed-type-defaults.phpt | 62 ++++++++ 8 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 phpunit/src/FixedTypeDefaultValueTest.php create mode 100644 src/Platform/Android.php create mode 100644 tests/compiler/basic/unset-fixed-type-defaults.phpt diff --git a/phpunit/src/Build/NativeBuildConfigurationTest.php b/phpunit/src/Build/NativeBuildConfigurationTest.php index 20fa5560..adf730a7 100644 --- a/phpunit/src/Build/NativeBuildConfigurationTest.php +++ b/phpunit/src/Build/NativeBuildConfigurationTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\TestCase; use TypePhp\CompilerTest; use TypePhp\Exception\TestError; use TypePhp\Platform\Ios; +use TypePhp\Platform\Android; use TypePhp\Platform\Linux; use TypePhp\Platform\Macos; use TypePhp\Platform\PlatformBase; @@ -114,6 +115,44 @@ final class NativeBuildConfigurationTest extends TestCase } } + public function testAndroidResolvesSelfContainedSdkAndSystemLibraries(): void + { + $phpxDir = $this->temporaryDirectory('phpx-android-host'); + $androidSdk = $this->temporaryDirectory('phpx-android-sdk'); + mkdir($androidSdk . '/lib', 0777, true); + mkdir($androidSdk . '/include/phpx', 0777, true); + file_put_contents( + $androidSdk . '/.typephp-android-sdk-abi', + "typephp-android-arm64-v8a-api24-phpx-sdk-abi-v1\n", + ); + $phpxArchive = $androidSdk . '/lib/libphpx.a'; + $phpArchive = $androidSdk . '/lib/libphp.a'; + touch($phpxArchive); + touch($phpArchive); + + $restorePhpx = $this->withEnvironment('PHPX_HOME', $phpxDir); + $restoreSdk = $this->withEnvironment('PHPX_ANDROID_SDK_DIR', $androidSdk); + try { + $compiler = $this->newCompiler(new Android()); + self::assertSame($phpxArchive, $compiler->findPhpxLibraryForTest()); + self::assertSame($androidSdk, $compiler->getPhpDir()); + self::assertSame([$androidSdk . '/lib'], $compiler->getLibraryPathsForTest()); + self::assertSame($androidSdk . '/include/phpx', $compiler->getIncludePathsForTest()[0]); + self::assertSame( + [$phpxArchive, $phpArchive, 'log', 'android', 'dl', 'm'], + $compiler->getLibrariesForTest(), + ); + self::assertContains('PHPX_ANDROID=1', $compiler->getCommonCompileOptionsForTest()['user_defines']); + } finally { + $restoreSdk(); + $restorePhpx(); + } + + self::assertTrue(Android::supportsTarget('aarch64-linux-android24')); + self::assertFalse(Android::supportsTarget('x86_64-linux-android24')); + self::assertSame(24, Android::getApiLevel('aarch64-linux-android24')); + } + public function testNativeModulesDoNotFallBackToStaticPhpx(): void { $phpxDir = $this->temporaryDirectory('phpx-static-module'); diff --git a/phpunit/src/FixedTypeDefaultValueTest.php b/phpunit/src/FixedTypeDefaultValueTest.php new file mode 100644 index 00000000..0aac14d3 --- /dev/null +++ b/phpunit/src/FixedTypeDefaultValueTest.php @@ -0,0 +1,23 @@ +isIosTarget() ? $this->getIosSdkDir() : null; + if ($this->isIosTarget()) { + return $this->getIosSdkDir(); + } + return $this->isAndroidTarget() ? $this->getAndroidSdkDir() : null; } protected function getIosSdkDir(): string @@ -68,6 +71,30 @@ trait NativeBuildConfigurationTrait return $sdkDir; } + protected function getAndroidSdkDir(): string + { + $configured = getenv('PHPX_ANDROID_SDK_DIR'); + $sdkDir = is_string($configured) && $configured !== '' + ? rtrim($configured, '/\\') + : $this->getPhpxDir() . '/android/arm64-v8a'; + if (!is_dir($sdkDir)) { + $this->error( + 'The Android SDK was not found at: ' . $sdkDir . "\n" + . ' Build it with PHPX sdk/build-native.sh or set PHPX_ANDROID_SDK_DIR.' + ); + } + $api = \TypePhp\Platform\Android::getApiLevel($this->targetPlatform); + $expected = "typephp-android-arm64-v8a-api{$api}-phpx-sdk-abi-v1"; + $abiStamp = $sdkDir . '/.typephp-android-sdk-abi'; + if (!is_file($abiStamp) || trim((string) file_get_contents($abiStamp)) !== $expected) { + $this->error( + 'The Android SDK is missing or ABI-incompatible: ' . $sdkDir . "\n" + . ' Rebuild it with the matching Android PHP Runtime Layer and NDK.' + ); + } + return $sdkDir; + } + /** * The target triple used for fully-static links. * @@ -231,6 +258,12 @@ trait NativeBuildConfigurationTrait $libraries[] = 'mpfr.lib'; $libraries[] = 'libmpdec-4.0.1.dll.lib'; $libraries[] = 'libmpdec++-4.0.1.dll.lib'; + } elseif ($this->isAndroidTarget()) { + $libraries[] = $this->getAndroidSdkDir() . '/lib/libphp.a'; + $libraries[] = 'log'; + $libraries[] = 'android'; + $libraries[] = 'dl'; + $libraries[] = 'm'; } else { // Unix PHP extensions resolve Zend/PHP symbols from the host SAPI. // Linking libphp.so here would load a second ZendVM and give PHPX a @@ -281,7 +314,7 @@ trait NativeBuildConfigurationTrait // PHPX_HOME/lib where host libraries are installed. Keeping libphp.a // and libphpx.a in the same prefix also prevents a host macOS archive // from being selected accidentally during an iOS link. - if ($this->isIosTarget()) { + if ($this->isIosTarget() || $this->isAndroidTarget()) { $phpxStaticPath = $this->getPhpDir() . '/lib/libphpx.a'; return is_file($phpxStaticPath) ? $phpxStaticPath : null; } @@ -318,6 +351,9 @@ trait NativeBuildConfigurationTrait } elseif ($this->isIosTarget()) { $expected = $this->getPhpDir() . '/lib/libphpx.a'; $buildHint = 'Build the integrated iPhoneOS SDK in PHPX_HOME/ios/iphoneos-arm64'; + } elseif ($this->isAndroidTarget()) { + $expected = $this->getAndroidSdkDir() . '/lib/libphpx.a'; + $buildHint = 'Build the Android SDK with PHPX sdk/build-native.sh'; } else { $sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.'); $expected = $this->getPhpxDir() . '/lib/libphpx.' . $sharedLibExt; diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index 0803901e..f113b8ee 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -26,6 +26,9 @@ trait NativeCommandOptionsTrait if ($this->isIosTarget()) { $userDefines[] = 'PHPX_IOS=1'; } + if ($this->isAndroidTarget()) { + $userDefines[] = 'PHPX_ANDROID=1'; + } if ($this->isBuildModeLib()) { $userDefines[] = 'TYPEPHP_NO_MAIN=1'; $userDefines[] = $this->getLibraryExportsMacroName() . '=1'; diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 4e841d09..96d40c3d 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -68,6 +68,7 @@ use TypePhp\Parser\UniversalMethodCall; use TypePhp\Optimizer\FuncCallOptimizer; use TypePhp\Platform\Linux; use TypePhp\Platform\Ios; +use TypePhp\Platform\Android; use TypePhp\Platform\Macos; use TypePhp\Platform\PlatformBase; use TypePhp\Platform\PlatformFactory; @@ -685,6 +686,11 @@ class CompilerBase implements PropertyAccessContext return $this->getPlatform() instanceof Ios; } + public function isAndroidTarget(): bool + { + return $this->getPlatform() instanceof Android; + } + public function isWasiTarget(): bool { $target = strtolower($this->targetPlatform); @@ -735,6 +741,9 @@ class CompilerBase implements PropertyAccessContext // of the integrated PHPX SDK, never the host PHP installation. return $this->getIosSdkDir(); } + if ($this->isAndroidTarget()) { + return $this->getAndroidSdkDir(); + } try { return $this->getPlatform()->getPhpDir(); diff --git a/src/Platform/Android.php b/src/Platform/Android.php new file mode 100644 index 00000000..d77957cd --- /dev/null +++ b/src/Platform/Android.php @@ -0,0 +1,134 @@ + 'darwin-x86_64', + 'Windows' => 'windows-x86_64', + default => 'linux-x86_64', + }; + $compiler = rtrim($ndkRoot, '/\\') + . '/toolchains/llvm/prebuilt/' . $hostTag . '/bin/clang++'; + if (!is_file($compiler) || !is_executable($compiler)) { + throw new \RuntimeException("Android NDK clang++ was not found: {$compiler}"); + } + return $compiler; + } + + public function getDefaultRpaths(?string $phpxDir = null, ?string $phpDir = null): array + { + return []; + } + + public function buildPhpIncludePaths(string $phpDir): array + { + $paths = [ + $phpDir . '/include/php', + $phpDir . '/include/php/main', + $phpDir . '/include/php/TSRM', + $phpDir . '/include/php/Zend', + $phpDir . '/include/php/ext', + $phpDir . '/include/php/ext/date/lib', + ]; + return array_values(array_filter($paths, is_dir(...))); + } + + public function buildPhpLibPaths(string $phpDir): array + { + $libDir = rtrim($phpDir, '/') . '/lib'; + return is_dir($libDir) ? [$libDir] : []; + } + + public function detectPhpLibs(string $phpDir): array + { + $archive = rtrim($phpDir, '/') . '/lib/libphp.a'; + if (!is_file($archive)) { + throw new \RuntimeException("The Android libphp.a was not found: {$archive}"); + } + return ['embed' => null, 'static' => $archive, 'is_shared' => false]; + } + + /** Keep the mutually dependent PHP/PHPX archives in one linker group. */ + public function getLibraryFlags(array $libraries): string + { + $archives = []; + $systemLibraries = []; + foreach ($libraries as $library) { + if (is_string($library) && str_ends_with($library, '.a') && is_file($library)) { + $archives[] = escapeshellarg($library); + } else { + $name = basename((string) $library); + if (str_starts_with($name, 'lib')) { + $name = substr($name, 3); + } + $systemLibraries[] = '-l' . preg_replace('/\.(?:a|so)$/', '', $name); + } + } + + $flags = []; + if ($archives !== []) { + $flags[] = '-Wl,--start-group ' . implode(' ', $archives) . ' -Wl,--end-group'; + } + return implode(' ', array_merge($flags, $systemLibraries)); + } + + public function getBuildLibraryWarnings( + string $phpDir, + string $phpxDir, + string $buildMode, + bool $checkPhpxRuntime = true, + ): array { + return []; + } +} diff --git a/src/Translator.php b/src/Translator.php index 00f1c54b..a1b30f77 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -44,6 +44,7 @@ use TypePhp\Generator\Symbol; use TypePhp\Metadata\Constants; use TypePhp\Platform\PlatformFactory; use TypePhp\Platform\Ios; +use TypePhp\Platform\Android; use TypePhp\Platform\Wasi; use TypePhp\Platform\Windows; use TypePhp\Resolver\Reflection; @@ -220,6 +221,8 @@ class Translator extends Preprocessor throw new \RuntimeException('The iOS target requires a macOS build host with Xcode'); } $this->platform = new Ios(); + } elseif (Android::supportsTarget($targetPlatform)) { + $this->platform = new Android(); } elseif ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') { $detectedTarget = getenv('TYPEPHP_WASI_TARGET'); $this->platform = new Wasi( @@ -2874,6 +2877,11 @@ CODE; $this->compilerBackend = null; $this->cppCompiler = $this->platform->getDefaultCompiler(); $this->initializeNewArchitecture(); + } elseif (Android::supportsTarget($this->targetPlatform)) { + $this->platform = new Android(); + $this->compilerBackend = null; + $this->cppCompiler = $this->platform->getDefaultCompiler(); + $this->initializeNewArchitecture(); } } diff --git a/tests/compiler/basic/unset-fixed-type-defaults.phpt b/tests/compiler/basic/unset-fixed-type-defaults.phpt new file mode 100644 index 00000000..356b1695 --- /dev/null +++ b/tests/compiler/basic/unset-fixed-type-defaults.phpt @@ -0,0 +1,62 @@ +--TEST-- +unset restores fixed typed variables to their initial states +--FILE-- + +--EXPECT-- +int(0) +float(0) +bool(false) +string(0) "" +array(0) { +} +NULL +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +array(1) { + [0]=> + int(7) +} +int(7)