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
master
韩天峰 1 month ago
parent f3e2e32648
commit 8dfb363422
  1. 39
      phpunit/src/Build/NativeBuildConfigurationTest.php
  2. 23
      phpunit/src/FixedTypeDefaultValueTest.php
  3. 40
      src/Build/NativeBuildConfigurationTrait.php
  4. 3
      src/Build/NativeCommandOptionsTrait.php
  5. 9
      src/CompilerBase.php
  6. 134
      src/Platform/Android.php
  7. 8
      src/Translator.php
  8. 62
      tests/compiler/basic/unset-fixed-type-defaults.phpt

@ -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');

@ -0,0 +1,23 @@
<?php
use PHPUnit\Framework\TestCase;
use TypePhp\Type;
final class FixedTypeDefaultValueTest extends TestCase
{
public function testFixedValueTypesExposeTheirInitialStateExpressions(): void
{
self::assertSame('0', Type::getDefaultValueExpression(Type::INT));
self::assertSame('0.0', Type::getDefaultValueExpression(Type::FLOAT));
self::assertSame('false', Type::getDefaultValueExpression(Type::BOOL));
self::assertSame('php::Str()', Type::getDefaultValueExpression(Type::STR));
self::assertSame('php::Array{}', Type::getDefaultValueExpression(Type::ARRAY));
}
public function testObjectAndDynamicStorageHaveSeparateEmptyStateRules(): void
{
self::assertNull(Type::getDefaultValueExpression(Type::OBJECT));
self::assertNull(Type::getDefaultValueExpression(Type::VAR));
self::assertNull(Type::getDefaultValueExpression(Type::REF));
}
}

@ -44,7 +44,10 @@ trait NativeBuildConfigurationTrait
return $fullStaticSdk;
}
return $this->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;

@ -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';

@ -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();

@ -0,0 +1,134 @@
<?php
namespace TypePhp\Platform;
/** Android arm64-v8a cross-compilation target. */
final class Android extends UnixPlatform
{
private const DEFAULT_API = 24;
public static function supportsTarget(string $target): bool
{
return preg_match('/^aarch64-linux-android(?:[0-9]+)?$/i', $target) === 1;
}
public static function getApiLevel(string $target = ''): int
{
if (preg_match('/^aarch64-linux-android([0-9]+)$/i', $target, $matches) === 1) {
return (int) $matches[1];
}
$configured = getenv('TYPEPHP_ANDROID_API');
return is_string($configured) && preg_match('/^[0-9]+$/', $configured) === 1
? (int) $configured
: self::DEFAULT_API;
}
public function getName(): string
{
return 'Android arm64-v8a';
}
public function isCurrent(): bool
{
return false;
}
public function getSharedLibraryExtension(): string
{
return '.so';
}
public function getSharedLinkFlag(): string
{
return '-shared -static-libstdc++'
. ' -Wl,-z,max-page-size=16384,-z,common-page-size=16384'
. ' -Wl,-z,relro,-z,now';
}
public function getDefaultCompiler(): string
{
$ndkRoot = getenv('ANDROID_NDK_HOME') ?: getenv('ANDROID_NDK_ROOT');
if (!is_string($ndkRoot) || $ndkRoot === '') {
throw new \RuntimeException('Set ANDROID_NDK_HOME to an Android NDK installation');
}
$hostTag = match (PHP_OS_FAMILY) {
'Darwin' => '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 [];
}
}

@ -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();
}
}

@ -0,0 +1,62 @@
--TEST--
unset restores fixed typed variables to their initial states
--FILE--
<?php
class UnsetFixedTypeObject
{
}
function makeUnsetArrayValue(): int
{
return 7;
}
function main(): void
{
$integer = 42;
$float = 2.5;
$boolean = true;
$string = 'value';
$array = [1, 2];
$object = new UnsetFixedTypeObject();
$dynamic = std::any('value');
unset($integer, $float, $boolean, $string, $array, $object, $dynamic);
var_dump($integer, $float, $boolean, $string, $array, $object);
var_dump(
isset($integer),
isset($float),
isset($boolean),
isset($string),
isset($array),
isset($object),
isset($dynamic),
);
$array[] = 5;
unset($array);
$appendResult = ($array[] = makeUnsetArrayValue());
var_dump($array, $appendResult);
}
?>
--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)
Loading…
Cancel
Save