From b3566368533942b67ebf6e69560d2c902abac6af Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 7 Sep 2026 16:53:18 +0800 Subject: [PATCH] feat(platform): add iOS target support for TypePHP native compilation - Add Ios platform class with iPhoneOS cross-compilation support - Implement iOS-specific build configuration and library handling - Add iOS build example with UIKit bridge implementation - Update compiler base to recognize iOS target platform - Add iOS-specific gitignore entries and build directories - Implement unified UI bridge functions with platform-agnostic naming - Add iOS project configuration and packaging script - Include iOS platform tests and documentation updates --- examples/objective-c-macos/.gitignore | 2 + examples/objective-c-macos/Info-iOS.plist | 42 +++++ examples/objective-c-macos/README.md | 81 +++++++-- .../objective-c-macos/ios-src/uikit_bridge.mm | 169 ++++++++++++++++++ .../ios-src/uikit_bridge.stub.php | 23 +++ examples/objective-c-macos/ios.yml | 24 +++ .../objc-src/macos_bridge.mm | 14 +- .../objc-src/macos_bridge.stub.php | 14 +- examples/objective-c-macos/package-ios-app.sh | 48 +++++ .../objective-c-macos/php-src/application.php | 41 +++++ .../objective-c-macos/php-src/ios-main.php | 19 ++ examples/objective-c-macos/php-src/main.php | 33 +--- examples/objective-c-macos/project.yml | 3 +- .../Build/NativeBuildConfigurationTest.php | 64 ++++++- phpunit/src/Platform/PlatformTest.php | 68 +++++++ src/Build/NativeBuildConfigurationTrait.php | 54 +++++- src/Build/NativeCommandOptionsTrait.php | 3 + src/CompilerBase.php | 12 ++ src/Platform/Ios.php | 111 ++++++++++++ src/Translator.php | 23 ++- 20 files changed, 782 insertions(+), 66 deletions(-) create mode 100644 examples/objective-c-macos/Info-iOS.plist create mode 100644 examples/objective-c-macos/ios-src/uikit_bridge.mm create mode 100644 examples/objective-c-macos/ios-src/uikit_bridge.stub.php create mode 100644 examples/objective-c-macos/ios.yml create mode 100755 examples/objective-c-macos/package-ios-app.sh create mode 100644 examples/objective-c-macos/php-src/application.php create mode 100644 examples/objective-c-macos/php-src/ios-main.php create mode 100644 src/Platform/Ios.php diff --git a/examples/objective-c-macos/.gitignore b/examples/objective-c-macos/.gitignore index da2cb165..014c319e 100644 --- a/examples/objective-c-macos/.gitignore +++ b/examples/objective-c-macos/.gitignore @@ -1,5 +1,7 @@ /build/ /build-*/ +/ios-build/ /dist/ /*.rsp /typephp_macos_hello +/typephp_ios_hello diff --git a/examples/objective-c-macos/Info-iOS.plist b/examples/objective-c-macos/Info-iOS.plist new file mode 100644 index 00000000..61df13f3 --- /dev/null +++ b/examples/objective-c-macos/Info-iOS.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + TypePHP Hello + CFBundleExecutable + typephp_ios_hello + CFBundleIdentifier + org.swoole.typephp.ios-hello + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + TypePHP iOS Hello + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + MinimumOSVersion + 15.0 + UIDeviceFamily + + 1 + + UILaunchScreen + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + diff --git a/examples/objective-c-macos/README.md b/examples/objective-c-macos/README.md index ab1692a9..7ff7515c 100644 --- a/examples/objective-c-macos/README.md +++ b/examples/objective-c-macos/README.md @@ -1,10 +1,12 @@ -# TypePHP macOS AppKit example +# TypePHP Apple native GUI examples -This is a native macOS GUI application built from TypePHP and Objective-C++. -TypePHP defines the window layout, application state, click behavior and event -loop. Objective-C++ remains a thin platform bridge that exposes primitive AppKit -operations. Clicking the native button returns a control ID to TypePHP, which -updates the status label. +This directory contains matching macOS AppKit and iPhoneOS UIKit applications. +Both targets reuse `php-src/application.php`: TypePHP defines the window layout, +application state and click behavior, while Objective-C++ only exposes small +native UI operations. Clicking the native button returns a control ID to +TypePHP, which updates the status label. + +## macOS Requirements: @@ -33,8 +35,65 @@ sh examples/objective-c-macos/package-app.sh open "examples/objective-c-macos/dist/TypePHP macOS Hello.app" ``` -The project uses an `.mm` source because PHPX exposes a C++ API. Its project -configuration enables ARC and links both AppKit and Foundation. This example is -macOS-only; an iOS application would additionally require Xcode project and -code-signing integration, so it is intentionally not mixed into this desktop -example. +## iPhoneOS (physical iPhone) + +The iPhone build is cross-compiled on macOS. It requires full Xcode (Command +Line Tools alone do not contain the iPhoneOS SDK), an Apple Development signing +identity, a provisioning profile for `org.swoole.typephp.ios-hello`, and an +`iphoneos-arm64` TypePHP SDK with this layout: + +```text +/ +├── include/php/... +├── lib/ +│ ├── libphp.a +│ ├── libphpx.a +│ ├── libgmp.a +│ ├── libgmpxx.a +│ └── libmpfr.a +└── .typephp-ios-sdk-abi +``` + +The PHP and every third-party archive in this prefix must be built for +`arm64-apple-ios`; a macOS/Homebrew archive cannot be linked into an iPhoneOS +binary. Apple system libraries and UIKit/Foundation remain SDK frameworks and +are linked by Xcode rather than copied into `libphp.a`. + +After selecting full Xcode, verify the SDK: + +```sh +sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer +xcrun --sdk iphoneos --show-sdk-path +``` + +Install this SDK at `ios/iphoneos-arm64` inside the PHPX checkout, matching the +integrated layouts already used by `full-static/sdk` and +`wasm/wasm32-wasip2`. Build the executable from the TypePHP repository root; +`PHPX_HOME` selects both PHPX sources and the target SDK: + +```sh +export PHPX_HOME=/path/to/phpx +php bin/tpc.php examples/objective-c-macos/ios.yml --no-progress +``` + +The compiler produces `examples/objective-c-macos/typephp_ios_hello`. Package +and sign it with a development provisioning profile and identity installed in +the login keychain: + +```sh +export TYPEPHP_IOS_PROVISIONING_PROFILE=/path/to/profile.mobileprovision +export TYPEPHP_IOS_CODE_SIGN_IDENTITY='Apple Development: Your Name (TEAMID)' +sh examples/objective-c-macos/package-ios-app.sh +``` + +Find the connected iPhone and install the bundle: + +```sh +xcrun devicectl list devices +xcrun devicectl device install app \ + --device \ + 'examples/objective-c-macos/dist/TypePHP iOS Hello.app' +``` + +The `.mm` bridges are deliberately thin. AppKit/UIKit own native controls and +deliver events, but the shared TypePHP class owns the application behavior. diff --git a/examples/objective-c-macos/ios-src/uikit_bridge.mm b/examples/objective-c-macos/ios-src/uikit_bridge.mm new file mode 100644 index 00000000..7a9dea96 --- /dev/null +++ b/examples/objective-c-macos/ios-src/uikit_bridge.mm @@ -0,0 +1,169 @@ +#include +#include + +void php_typephp_application_did_launch(); +void php_typephp_application_control_activated(php::Int controlId); + +static NSString *to_ns_string(const php::Str &value) +{ + NSString *result = [[NSString alloc] initWithBytes:value.data() + length:value.length() + encoding:NSUTF8StringEncoding]; + return result == nil ? @"" : result; +} + +@interface TypePHPUIKitBridge : NSObject + +@property(nonatomic, strong) UIWindow *window; +@property(nonatomic, strong) UIViewController *rootViewController; +@property(nonatomic, strong) NSMutableDictionary *controls; +@property(nonatomic) NSInteger nextControlId; +@property(nonatomic) CGFloat logicalWidth; +@property(nonatomic) CGFloat logicalHeight; + +- (void)controlActivated:(UIControl *)sender; + +@end + +static __strong TypePHPUIKitBridge *bridge = nil; +static __strong NSString *applicationName = nil; + +@implementation TypePHPUIKitBridge + +- (instancetype)init +{ + self = [super init]; + if (self != nil) { + self.controls = [[NSMutableDictionary alloc] init]; + self.nextControlId = 1; + } + return self; +} + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + (void) application; + (void) launchOptions; + bridge = self; + php_typephp_application_did_launch(); + return YES; +} + +- (void)controlActivated:(UIControl *)sender +{ + php_typephp_application_control_activated(static_cast(sender.tag)); +} + +@end + + +static CGRect logical_rect(php::Int x, php::Int y, php::Int width, php::Int height) +{ + const CGRect bounds = bridge.rootViewController.view.bounds; + const CGFloat scaleX = CGRectGetWidth(bounds) / bridge.logicalWidth; + const CGFloat scaleY = CGRectGetHeight(bounds) / bridge.logicalHeight; + const CGFloat top = bridge.logicalHeight - static_cast(y + height); + return CGRectMake( + static_cast(x) * scaleX, + top * scaleY, + static_cast(width) * scaleX, + static_cast(height) * scaleY); +} + +static php::Int store_control(UIView *control) +{ + const NSInteger controlId = bridge.nextControlId++; + control.tag = controlId; + bridge.controls[@(controlId)] = control; + return static_cast(controlId); +} + +void php_ui_app_run(php::Str name) +{ + @autoreleasepool { + applicationName = to_ns_string(name); + UIApplicationMain(0, nullptr, nil, NSStringFromClass(TypePHPUIKitBridge.class)); + } +} + +void php_ui_create_window(php::Str title, php::Int width, php::Int height) +{ + @autoreleasepool { + (void) title; + bridge.logicalWidth = static_cast(width); + bridge.logicalHeight = static_cast(height); + bridge.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + bridge.rootViewController = [[UIViewController alloc] init]; + bridge.rootViewController.title = applicationName; + bridge.rootViewController.view.backgroundColor = UIColor.systemBackgroundColor; + bridge.window.rootViewController = bridge.rootViewController; + } +} + +php::Int php_ui_add_label( + php::Str text, + php::Int x, + php::Int y, + php::Int width, + php::Int height, + php::Int fontSize, + php::Bool bold) +{ + @autoreleasepool { + UILabel *label = [[UILabel alloc] initWithFrame:logical_rect(x, y, width, height)]; + label.text = to_ns_string(text); + label.font = bold + ? [UIFont systemFontOfSize:static_cast(fontSize) + weight:UIFontWeightSemibold] + : [UIFont systemFontOfSize:static_cast(fontSize)]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + label.adjustsFontSizeToFitWidth = YES; + label.minimumScaleFactor = 0.75; + [bridge.rootViewController.view addSubview:label]; + return store_control(label); + } +} + +php::Int php_ui_add_button( + php::Str title, + php::Int x, + php::Int y, + php::Int width, + php::Int height) +{ + @autoreleasepool { + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + button.frame = logical_rect(x, y, width, height); + [button setTitle:to_ns_string(title) forState:UIControlStateNormal]; + button.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + button.backgroundColor = UIColor.secondarySystemBackgroundColor; + button.layer.cornerRadius = 10; + [button addTarget:bridge + action:@selector(controlActivated:) + forControlEvents:UIControlEventTouchUpInside]; + [bridge.rootViewController.view addSubview:button]; + return store_control(button); + } +} + +void php_ui_set_control_text(php::Int controlId, php::Str text) +{ + @autoreleasepool { + UIView *control = bridge.controls[@(static_cast(controlId))]; + NSString *value = to_ns_string(text); + if ([control isKindOfClass:UILabel.class]) { + ((UILabel *) control).text = value; + } else if ([control isKindOfClass:UIButton.class]) { + [(UIButton *) control setTitle:value forState:UIControlStateNormal]; + } + } +} + +void php_ui_show_window() +{ + @autoreleasepool { + [bridge.window makeKeyAndVisible]; + } +} diff --git a/examples/objective-c-macos/ios-src/uikit_bridge.stub.php b/examples/objective-c-macos/ios-src/uikit_bridge.stub.php new file mode 100644 index 00000000..7ec56f2a --- /dev/null +++ b/examples/objective-c-macos/ios-src/uikit_bridge.stub.php @@ -0,0 +1,23 @@ +(width), static_cast(height)); @@ -103,7 +103,7 @@ void php_macos_create_window(php::Str title, php::Int width, php::Int height) } } -php::Int php_macos_add_label( +php::Int php_ui_add_label( php::Str text, php::Int x, php::Int y, @@ -130,7 +130,7 @@ php::Int php_macos_add_label( } } -php::Int php_macos_add_button( +php::Int php_ui_add_button( php::Str title, php::Int x, php::Int y, @@ -154,7 +154,7 @@ php::Int php_macos_add_button( } } -void php_macos_set_control_text(php::Int controlId, php::Str text) +void php_ui_set_control_text(php::Int controlId, php::Str text) { @autoreleasepool { NSControl *control = bridge.controls[@(static_cast(controlId))]; @@ -164,7 +164,7 @@ void php_macos_set_control_text(php::Int controlId, php::Str text) } } -void php_macos_show_window() +void php_ui_show_window() { @autoreleasepool { [bridge.window makeKeyAndOrderFront:nil]; @@ -173,7 +173,7 @@ void php_macos_show_window() } } -php::Int php_macos_next_event() +php::Int php_ui_next_event() { while (bridge.running) { @autoreleasepool { diff --git a/examples/objective-c-macos/objc-src/macos_bridge.stub.php b/examples/objective-c-macos/objc-src/macos_bridge.stub.php index 10224237..4afc0e9c 100644 --- a/examples/objective-c-macos/objc-src/macos_bridge.stub.php +++ b/examples/objective-c-macos/objc-src/macos_bridge.stub.php @@ -1,11 +1,11 @@ &2 + exit 1 +fi + +if [ -z "$profile" ] || [ ! -f "$profile" ]; then + echo "Set TYPEPHP_IOS_PROVISIONING_PROFILE to an iOS Development .mobileprovision file." >&2 + exit 1 +fi + +if [ -z "$identity" ]; then + echo "Set TYPEPHP_IOS_CODE_SIGN_IDENTITY to an installed Apple Development identity." >&2 + exit 1 +fi + +profile_plist=$(mktemp -t typephp-ios-profile.XXXXXX) +entitlements=$(mktemp -t typephp-ios-entitlements.XXXXXX) +trap 'rm -f "$profile_plist" "$entitlements"' EXIT HUP INT TERM + +security cms -D -i "$profile" -o "$profile_plist" +plutil -extract Entitlements xml1 -o "$entitlements" "$profile_plist" + +rm -rf "$bundle" +mkdir -p "$bundle" +cp "$project_dir/Info-iOS.plist" "$bundle/Info.plist" +cp "$executable" "$bundle/typephp_ios_hello" +cp "$profile" "$bundle/embedded.mobileprovision" + +codesign \ + --force \ + --sign "$identity" \ + --entitlements "$entitlements" \ + --timestamp=none \ + "$bundle" +codesign --verify --deep --strict --verbose=2 "$bundle" + +echo "Created and signed $bundle" +echo "Install it with: xcrun devicectl device install app --device '$bundle'" diff --git a/examples/objective-c-macos/php-src/application.php b/examples/objective-c-macos/php-src/application.php new file mode 100644 index 00000000..c6afffde --- /dev/null +++ b/examples/objective-c-macos/php-src/application.php @@ -0,0 +1,41 @@ +temporaryDirectory('phpx-ios-host'); + mkdir($phpxDir . '/lib', 0777, true); + // A host-side archive with the same name must never win. + touch($phpxDir . '/lib/libphpx.a'); + + $iosSdk = $phpxDir . '/ios/iphoneos-arm64'; + mkdir($iosSdk . '/lib', 0777, true); + file_put_contents($iosSdk . '/.typephp-ios-sdk-abi', "typephp-iphoneos-arm64-sdk-abi-v1\n"); + $targetArchive = $iosSdk . '/lib/libphpx.a'; + touch($targetArchive); + + $restorePhpx = $this->withEnvironment('PHPX_HOME', $phpxDir); + $restorePhp = $this->withEnvironment('PHP_HOME', $this->temporaryDirectory('unrelated-host-php')); + try { + $compiler = $this->newCompiler(new Ios()); + self::assertSame($targetArchive, $compiler->findPhpxLibraryForTest()); + self::assertSame($iosSdk, $compiler->getPhpDir()); + self::assertSame([$iosSdk . '/lib'], $compiler->getLibraryPathsForTest()); + self::assertSame($iosSdk . '/include/phpx', $compiler->getIncludePathsForTest()[0]); + self::assertNotContains($phpxDir . '/include', $compiler->getIncludePathsForTest()); + self::assertSame( + [$targetArchive, 'php', 'gmp', 'gmpxx', 'mpfr', 'c++'], + $compiler->getLibrariesForTest(), + ); + self::assertContains('PHPX_IOS=1', $compiler->getCommonCompileOptionsForTest()['user_defines']); + } finally { + $restorePhp(); + $restorePhpx(); + } + } + public function testNativeModulesDoNotFallBackToStaticPhpx(): void { $phpxDir = $this->temporaryDirectory('phpx-static-module'); @@ -175,6 +209,21 @@ final class NativeBuildConfigurationTest extends TestCase { return $this->getLibraries(); } + + public function getIncludePathsForTest(): array + { + return $this->getIncludePaths(); + } + + public function getLibraryPathsForTest(): array + { + return $this->getLibraryPaths(); + } + + public function getCommonCompileOptionsForTest(): array + { + return $this->getCommonCompileCommandOptions()->toArray(); + } }; return $compiler->withPlatform($platform); @@ -182,13 +231,18 @@ final class NativeBuildConfigurationTest extends TestCase private function withPhpxHome(string $dir): callable { - $previous = getenv('PHPX_HOME'); - putenv('PHPX_HOME=' . $dir); - return static function () use ($previous): void { + return $this->withEnvironment('PHPX_HOME', $dir); + } + + private function withEnvironment(string $name, string $value): callable + { + $previous = getenv($name); + putenv($name . '=' . $value); + return static function () use ($name, $previous): void { if ($previous === false) { - putenv('PHPX_HOME'); + putenv($name); } else { - putenv('PHPX_HOME=' . $previous); + putenv($name . '=' . $previous); } }; } diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index 3106e85d..baaad8ae 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -5,6 +5,7 @@ namespace TypePhp\Tests\Platform; use PHPUnit\Framework\TestCase; use TypePhp\Platform\Windows; use TypePhp\Platform\Linux; +use TypePhp\Platform\Ios; use TypePhp\Platform\Macos; use TypePhp\Platform\Wasi; @@ -22,6 +23,71 @@ class PlatformTest extends TestCase $this->assertSame([], $platform->getBuildLibraryWarnings('', '', 'bin')); } + public function testIosTargetProperties(): void + { + $platform = new Ios(); + + $this->assertSame('iOS', $platform->getName()); + $this->assertSame('', $platform->getExecutableExtension()); + $this->assertSame('.a', $platform->getSharedLibraryExtension()); + $this->assertSame('xcrun --sdk iphoneos clang++', $platform->getDefaultCompiler()); + $this->assertSame([], $platform->getDefaultRpaths('/tmp/phpx', '/tmp/php')); + $this->assertTrue(Ios::supportsTarget('arm64-apple-ios15.0')); + $this->assertTrue(Ios::supportsTarget('aarch64-apple-ios')); + $this->assertFalse(Ios::supportsTarget('arm64-apple-darwin')); + $this->assertFalse(Ios::supportsTarget('x86_64-apple-ios15.0-simulator')); + } + + public function testIosLibraryDiagnosticsUseTargetSdkPrefix(): void + { + $sdk = sys_get_temp_dir() . '/typephp-ios-sdk-' . bin2hex(random_bytes(6)); + mkdir($sdk . '/include/php/main', 0777, true); + mkdir($sdk . '/include/php/TSRM', 0777, true); + mkdir($sdk . '/include/php/Zend', 0777, true); + mkdir($sdk . '/include/php/ext/date/lib', 0777, true); + mkdir($sdk . '/lib', 0777, true); + touch($sdk . '/lib/libphp.a'); + touch($sdk . '/lib/libphpx.a'); + + try { + $platform = new Ios(); + self::assertSame([], $platform->getBuildLibraryWarnings($sdk, '/unused/phpx-home', 'bin')); + self::assertSame([$sdk . '/lib'], $platform->buildPhpLibPaths($sdk)); + self::assertSame( + [ + $sdk . '/include/php', + $sdk . '/include/php/main', + $sdk . '/include/php/TSRM', + $sdk . '/include/php/Zend', + $sdk . '/include/php/ext', + $sdk . '/include/php/ext/date/lib', + ], + $platform->buildPhpIncludePaths($sdk), + ); + self::assertSame( + [ + 'embed' => null, + 'static' => $sdk . '/lib/libphp.a', + 'is_shared' => false, + ], + $platform->detectPhpLibs($sdk), + ); + } finally { + unlink($sdk . '/lib/libphpx.a'); + unlink($sdk . '/lib/libphp.a'); + rmdir($sdk . '/lib'); + rmdir($sdk . '/include/php/ext/date/lib'); + rmdir($sdk . '/include/php/ext/date'); + rmdir($sdk . '/include/php/ext'); + rmdir($sdk . '/include/php/Zend'); + rmdir($sdk . '/include/php/TSRM'); + rmdir($sdk . '/include/php/main'); + rmdir($sdk . '/include/php'); + rmdir($sdk . '/include'); + rmdir($sdk); + } + } + /** * 测试 Windows 平台基本功能 */ @@ -141,6 +207,7 @@ class PlatformTest extends TestCase $windows = new Windows(); $linux = new Linux(); $macos = new Macos(); + $ios = new Ios(); $this->assertSame('.exe', $windows->getTargetExtension('bin')); $this->assertSame('.dll', $windows->getTargetExtension('ext')); @@ -151,6 +218,7 @@ class PlatformTest extends TestCase $this->assertSame('', $macos->getTargetExtension('bin')); $this->assertSame('.so', $macos->getTargetExtension('ext')); $this->assertSame('.dylib', $macos->getTargetExtension('lib')); + $this->assertSame('', $ios->getTargetExtension('bin')); } public function testPlatformPathPrefixRemoval(): void diff --git a/src/Build/NativeBuildConfigurationTrait.php b/src/Build/NativeBuildConfigurationTrait.php index a479c9ad..b05e10ec 100644 --- a/src/Build/NativeBuildConfigurationTrait.php +++ b/src/Build/NativeBuildConfigurationTrait.php @@ -32,6 +32,42 @@ trait NativeBuildConfigurationTrait return $sdkDir; } + /** + * Resolve the SDK prefix used by targets that cannot consume host PHPX or + * host PHP libraries. Keep the target archive and all ABI-sensitive + * headers under the same PHPX checkout. + */ + protected function getTargetSdkDir(): ?string + { + $fullStaticSdk = $this->getFullStaticSdkDir(); + if ($fullStaticSdk !== null) { + return $fullStaticSdk; + } + + return $this->isIosTarget() ? $this->getIosSdkDir() : null; + } + + protected function getIosSdkDir(): string + { + $sdkDir = $this->getPhpxDir() . '/ios/iphoneos-arm64'; + if (!is_dir($sdkDir)) { + $this->error( + 'The iPhoneOS SDK was not found at: ' . $sdkDir . "\n" + . ' Build/install the matching SDK inside PHPX before compiling this target.' + ); + } + $abiStamp = $sdkDir . '/.typephp-ios-sdk-abi'; + if (!is_file($abiStamp) + || trim((string) file_get_contents($abiStamp)) !== 'typephp-iphoneos-arm64-sdk-abi-v1' + ) { + $this->error( + 'The iPhoneOS SDK is missing or ABI-incompatible: ' . $sdkDir . "\n" + . ' Rebuild it with PHPX ios/build.sh and the matching PHP SDK.' + ); + } + return $sdkDir; + } + /** * The target triple used for fully-static links. * @@ -75,7 +111,7 @@ trait NativeBuildConfigurationTrait protected function getIncludePaths(): array { - $sdkDir = $this->getFullStaticSdkDir(); + $sdkDir = $this->getTargetSdkDir(); if ($sdkDir !== null) { return [ $sdkDir . '/include/phpx', @@ -116,7 +152,7 @@ trait NativeBuildConfigurationTrait protected function getLibraryPaths(): array { - $sdkDir = $this->getFullStaticSdkDir(); + $sdkDir = $this->getTargetSdkDir(); if ($sdkDir !== null) { return [$sdkDir . '/lib']; } @@ -210,7 +246,7 @@ trait NativeBuildConfigurationTrait // do. WASI needs nothing — its toolchain supplies the runtime. if ($this->isLinux()) { $libraries[] = 'stdc++'; - } elseif ($this->isMacos()) { + } elseif ($this->isMacos() || $this->isIosTarget()) { $libraries[] = 'c++'; } } @@ -241,6 +277,15 @@ trait NativeBuildConfigurationTrait return is_file($phpxLibPath) ? $phpxLibPath : null; } + // An iPhoneOS archive belongs to the cross-compiled PHP SDK, not to + // 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()) { + $phpxStaticPath = $this->getPhpDir() . '/lib/libphpx.a'; + return is_file($phpxStaticPath) ? $phpxStaticPath : null; + } + // Linux/macOS: prefer the shared library, fall back to the static library // getSharedLibraryExtension() may or may not include a leading dot, so normalize it $sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.'); @@ -270,6 +315,9 @@ trait NativeBuildConfigurationTrait if ($platform instanceof Windows) { $expected = $this->getPhpxDir() . '\\lib\\phpx.lib'; $buildHint = 'Build PHPX first (for example, run `nmake phpx` in ' . $this->getPhpxDir() . '\\build)'; + } elseif ($this->isIosTarget()) { + $expected = $this->getPhpDir() . '/lib/libphpx.a'; + $buildHint = 'Build the integrated iPhoneOS SDK in PHPX_HOME/ios/iphoneos-arm64'; } else { $sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.'); $expected = $this->getPhpxDir() . '/lib/libphpx.' . $sharedLibExt; diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index 39499129..0803901e 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -23,6 +23,9 @@ trait NativeCommandOptionsTrait } $userDefines = $this->userDefines; + if ($this->isIosTarget()) { + $userDefines[] = 'PHPX_IOS=1'; + } if ($this->isBuildModeLib()) { $userDefines[] = 'TYPEPHP_NO_MAIN=1'; $userDefines[] = $this->getLibraryExportsMacroName() . '=1'; diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 554875d7..3ef2a3a8 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -67,6 +67,7 @@ use TypePhp\Parser\UnaryExpressionTrait; use TypePhp\Parser\UniversalMethodCall; use TypePhp\Optimizer\FuncCallOptimizer; use TypePhp\Platform\Linux; +use TypePhp\Platform\Ios; use TypePhp\Platform\Macos; use TypePhp\Platform\PlatformBase; use TypePhp\Platform\PlatformFactory; @@ -679,6 +680,11 @@ class CompilerBase implements PropertyAccessContext return $this->getPlatform() instanceof Macos; } + public function isIosTarget(): bool + { + return $this->getPlatform() instanceof Ios; + } + public function isWasiTarget(): bool { $target = strtolower($this->targetPlatform); @@ -724,6 +730,12 @@ class CompilerBase implements PropertyAccessContext public function getPhpDir(): string { + if ($this->isIosTarget()) { + // iPhoneOS is a cross target. Its PHP headers and archive are part + // of the integrated PHPX SDK, never the host PHP installation. + return $this->getIosSdkDir(); + } + try { return $this->getPlatform()->getPhpDir(); } catch (\RuntimeException $e) { diff --git a/src/Platform/Ios.php b/src/Platform/Ios.php new file mode 100644 index 00000000..e2ec7d83 --- /dev/null +++ b/src/Platform/Ios.php @@ -0,0 +1,111 @@ + null, + 'static' => $archive, + 'is_shared' => false, + ]; + } + + public function getBuildLibraryWarnings( + string $phpDir, + string $phpxDir, + string $buildMode, + bool $checkPhpxRuntime = true, + ): array { + if ($buildMode !== 'bin') { + return []; + } + + $warnings = []; + try { + $this->detectPhpLibs($phpDir); + } catch (\RuntimeException $exception) { + $warnings[] = [ + 'warning' => 'The iPhoneOS `libphp.a` is not found', + 'info' => $exception->getMessage() . '. Rebuild the iPhoneOS SDK in the PHPX directory', + ]; + } + + if (!is_file(rtrim($phpDir, '/') . '/lib/libphpx.a')) { + $warnings[] = [ + 'warning' => 'The iPhoneOS `libphpx.a` is not found', + 'info' => 'Build PHPX for iPhoneOS and install it into PHPX_HOME/ios/iphoneos-arm64', + ]; + } + + return $warnings; + } +} diff --git a/src/Translator.php b/src/Translator.php index 16547960..1feb84fb 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -42,6 +42,7 @@ use TypePhp\Generator\LibraryImportStubGenerator; use TypePhp\Generator\Symbol; use TypePhp\Metadata\Constants; use TypePhp\Platform\PlatformFactory; +use TypePhp\Platform\Ios; use TypePhp\Platform\Wasi; use TypePhp\Platform\Windows; use TypePhp\Resolver\Reflection; @@ -213,7 +214,12 @@ class Translator extends Preprocessor if ($targetPlatform === 'wasm32-wasip1' || $targetPlatform === 'wasm32-wasi') { throw new \RuntimeException('WASI Preview 1 is not supported; use wasm32-wasip2'); } - if ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') { + if (Ios::supportsTarget($targetPlatform)) { + if (strtoupper(substr(PHP_OS, 0, 6)) !== 'DARWIN') { + throw new \RuntimeException('The iOS target requires a macOS build host with Xcode'); + } + $this->platform = new Ios(); + } elseif ($targetPlatform === 'wasm32-wasip2' || $targetPlatform === 'wasm32-unknown-wasip2') { $detectedTarget = getenv('TYPEPHP_WASI_TARGET'); $this->platform = new Wasi( is_string($detectedTarget) && $detectedTarget !== '' ? $detectedTarget : $targetPlatform, @@ -1026,7 +1032,7 @@ class Translator extends Preprocessor $code .= 'extern "C" void save_ps_args(int, char **) {}' . PHP_EOL; } - if ($this->isBuildModeBin() && !$this->isWasiTarget()) { + if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) { $cliHeaders = [ '#include "php_cli_process_title.h"', '#include "php_cli_process_title_arginfo.h"', @@ -1225,7 +1231,7 @@ CODE; $code .= "// clang-format off\n"; $code .= "static const zend_function_entry ext_functions[] = {\n"; - if ($this->isBuildModeBin() && !$this->isWasiTarget()) { + if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) { $code .= $this->getIndent() . "PHP_FE(cli_set_process_title, arginfo_cli_set_process_title)\n"; $code .= $this->getIndent() . "PHP_FE(cli_get_process_title, arginfo_cli_get_process_title)\n"; } @@ -1808,7 +1814,7 @@ CODE; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc'; } - if ($this->isBuildModeBin() && !$this->isWasiTarget()) { + if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) { $sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c'; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c'; } @@ -2859,6 +2865,15 @@ CODE; $targetPlatform = $cfg['target-platform'] ?? null; if (!empty($targetPlatform)) { $this->targetPlatform = (string) $targetPlatform; + if (Ios::supportsTarget($this->targetPlatform)) { + if (strtoupper(substr(PHP_OS, 0, 6)) !== 'DARWIN') { + $this->error('The iOS target requires a macOS build host with Xcode'); + } + $this->platform = new Ios(); + $this->compilerBackend = null; + $this->cppCompiler = $this->platform->getDefaultCompiler(); + $this->initializeNewArchitecture(); + } } // Read build-dir