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
master
韩天峰 1 month ago
parent 622d6b0a00
commit b356636853
  1. 2
      examples/objective-c-macos/.gitignore
  2. 42
      examples/objective-c-macos/Info-iOS.plist
  3. 81
      examples/objective-c-macos/README.md
  4. 169
      examples/objective-c-macos/ios-src/uikit_bridge.mm
  5. 23
      examples/objective-c-macos/ios-src/uikit_bridge.stub.php
  6. 24
      examples/objective-c-macos/ios.yml
  7. 14
      examples/objective-c-macos/objc-src/macos_bridge.mm
  8. 14
      examples/objective-c-macos/objc-src/macos_bridge.stub.php
  9. 48
      examples/objective-c-macos/package-ios-app.sh
  10. 41
      examples/objective-c-macos/php-src/application.php
  11. 19
      examples/objective-c-macos/php-src/ios-main.php
  12. 33
      examples/objective-c-macos/php-src/main.php
  13. 3
      examples/objective-c-macos/project.yml
  14. 64
      phpunit/src/Build/NativeBuildConfigurationTest.php
  15. 68
      phpunit/src/Platform/PlatformTest.php
  16. 54
      src/Build/NativeBuildConfigurationTrait.php
  17. 3
      src/Build/NativeCommandOptionsTrait.php
  18. 12
      src/CompilerBase.php
  19. 111
      src/Platform/Ios.php
  20. 23
      src/Translator.php

@ -1,5 +1,7 @@
/build/
/build-*/
/ios-build/
/dist/
/*.rsp
/typephp_macos_hello
/typephp_ios_hello

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>TypePHP Hello</string>
<key>CFBundleExecutable</key>
<string>typephp_ios_hello</string>
<key>CFBundleIdentifier</key>
<string>org.swoole.typephp.ios-hello</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>TypePHP iOS Hello</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>15.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
</array>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

@ -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
<sdk>/
├── 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 <device-id> \
'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.

@ -0,0 +1,169 @@
#include <UIKit/UIKit.h>
#include <phpx.h>
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 <UIApplicationDelegate>
@property(nonatomic, strong) UIWindow *window;
@property(nonatomic, strong) UIViewController *rootViewController;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, UIView *> *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<UIApplicationLaunchOptionsKey, id> *)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<php::Int>(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<CGFloat>(y + height);
return CGRectMake(
static_cast<CGFloat>(x) * scaleX,
top * scaleY,
static_cast<CGFloat>(width) * scaleX,
static_cast<CGFloat>(height) * scaleY);
}
static php::Int store_control(UIView *control)
{
const NSInteger controlId = bridge.nextControlId++;
control.tag = controlId;
bridge.controls[@(controlId)] = control;
return static_cast<php::Int>(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<CGFloat>(width);
bridge.logicalHeight = static_cast<CGFloat>(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<CGFloat>(fontSize)
weight:UIFontWeightSemibold]
: [UIFont systemFontOfSize:static_cast<CGFloat>(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<NSInteger>(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];
}
}

@ -0,0 +1,23 @@
<?php
/** Enter the UIKit-owned iOS application lifecycle. */
function ui_app_run(string $applicationName): void {}
/** Thin wrappers around UIKit; coordinates use the shared 640x380 logical canvas. */
function ui_create_window(string $title, int $width, int $height): void {}
function ui_add_label(
string $text,
int $x,
int $y,
int $width,
int $height,
int $fontSize,
bool $bold
): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function ui_set_control_text(int $controlId, string $text): void {}
function ui_show_window(): void {}

@ -0,0 +1,24 @@
name: typephp_ios_hello
version: 0.1.0
mode: bin
output: typephp_ios_hello
php-version: "8.5"
cxx-std: c++17
cpp-compiler: xcrun --sdk iphoneos clang++
target-platform: arm64-apple-ios15.0
build-dir: ios-build
sources:
- php-src/application.php
- php-src/ios-main.php
- ios-src
cxx-flags:
- -fobjc-arc
- -miphoneos-version-min=15.0
ld-flags:
- -miphoneos-version-min=15.0
- -Wl,-dead_strip
- -framework UIKit
- -framework Foundation

@ -72,7 +72,7 @@ static NSInteger store_control(NSControl *control)
return controlId;
}
void php_macos_app_init(php::Str applicationName)
void php_ui_app_init(php::Str applicationName)
{
@autoreleasepool {
NSApplication *application = NSApplication.sharedApplication;
@ -83,7 +83,7 @@ void php_macos_app_init(php::Str applicationName)
}
}
void php_macos_create_window(php::Str title, php::Int width, php::Int height)
void php_ui_create_window(php::Str title, php::Int width, php::Int height)
{
@autoreleasepool {
const NSRect frame = NSMakeRect(0, 0, static_cast<CGFloat>(width), static_cast<CGFloat>(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<NSInteger>(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 {

@ -1,11 +1,11 @@
<?php
/** Thin wrappers around the macOS AppKit API. */
function macos_app_init(string $applicationName): void {}
function ui_app_init(string $applicationName): void {}
function macos_create_window(string $title, int $width, int $height): void {}
function ui_create_window(string $title, int $width, int $height): void {}
function macos_add_label(
function ui_add_label(
string $text,
int $x,
int $y,
@ -15,11 +15,11 @@ function macos_add_label(
bool $bold
): int {}
function macos_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function ui_add_button(string $title, int $x, int $y, int $width, int $height): int {}
function macos_set_control_text(int $controlId, string $text): void {}
function ui_set_control_text(int $controlId, string $text): void {}
function macos_show_window(): void {}
function ui_show_window(): void {}
/** Return the activated control ID, or -1 after the window is closed. */
function macos_next_event(): int {}
function ui_next_event(): int {}

@ -0,0 +1,48 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
executable="$project_dir/typephp_ios_hello"
bundle="$project_dir/dist/TypePHP iOS Hello.app"
profile=${TYPEPHP_IOS_PROVISIONING_PROFILE:-}
identity=${TYPEPHP_IOS_CODE_SIGN_IDENTITY:-}
if [ ! -x "$executable" ]; then
echo "Missing $executable; build ios.yml first." >&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 <device-id> '$bundle'"

@ -0,0 +1,41 @@
<?php
final class HelloApplication
{
private static int $statusLabel = 0;
private static int $button = 0;
private static int $clickCount = 0;
public static function build(string $platformApi): void
{
ui_create_window('TypePHP Native Hello', 640, 380);
ui_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true);
ui_add_label(
"The interface and application state are implemented in TypePHP.\n" .
"Objective-C++ only provides thin wrappers around $platformApi.",
64,
178,
512,
88,
15,
false,
);
self::$statusLabel = ui_add_label('Waiting for a native UI event.', 64, 130, 512, 24, 13, false);
self::$button = ui_add_button('Send event to TypePHP', 210, 64, 220, 40);
}
public static function handleEvent(int $controlId): void
{
if ($controlId !== self::$button) {
return;
}
self::$clickCount++;
$suffix = self::$clickCount === 1 ? 'time' : 'times';
ui_set_control_text(
self::$statusLabel,
'Button clicked ' . self::$clickCount . " $suffix; handled by TypePHP.",
);
}
}

@ -0,0 +1,19 @@
<?php
/** Called by the thin UIKit application delegate after iOS finishes launching. */
function typephp_application_did_launch(): void
{
HelloApplication::build('UIKit');
ui_show_window();
}
/** Called by the UIKit target-action bridge for every activated control. */
function typephp_application_control_activated(int $controlId): void
{
HelloApplication::handleEvent($controlId);
}
function main(): void
{
ui_app_run('TypePHP iOS Hello');
}

@ -2,37 +2,14 @@
function main(): void
{
macos_app_init('TypePHP macOS Hello');
macos_create_window('TypePHP macOS Hello', 640, 380);
macos_add_label('TypePHP + Objective-C++', 44, 292, 552, 40, 28, true);
macos_add_label(
"The application state and event loop are implemented in TypePHP.\n" .
'Objective-C++ only provides thin wrappers around AppKit.',
64,
178,
512,
88,
15,
false,
);
$statusLabel = macos_add_label('Waiting for an AppKit event.', 64, 130, 512, 24, 13, false);
$button = macos_add_button('Send event to TypePHP', 210, 64, 220, 40);
$clickCount = 0;
macos_show_window();
ui_app_init('TypePHP macOS Hello');
HelloApplication::build('AppKit');
ui_show_window();
while (true) {
$controlId = macos_next_event();
$controlId = ui_next_event();
if ($controlId < 0) {
break;
}
if ($controlId !== $button) {
continue;
}
$clickCount++;
$suffix = $clickCount === 1 ? 'time' : 'times';
macos_set_control_text($statusLabel, "Button clicked $clickCount $suffix; handled by TypePHP.");
HelloApplication::handleEvent($controlId);
}
}

@ -5,7 +5,8 @@ output: typephp_macos_hello
cxx-std: c++17
sources:
- php-src
- php-src/application.php
- php-src/main.php
- objc-src
cxx-flags:

@ -5,6 +5,7 @@ namespace TypePhp\Tests\Build;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
use TypePhp\Platform\Ios;
use TypePhp\Platform\Linux;
use TypePhp\Platform\Macos;
use TypePhp\Platform\PlatformBase;
@ -80,6 +81,39 @@ final class NativeBuildConfigurationTest extends TestCase
}
}
public function testIosResolvesPhpxArchiveFromIntegratedPhpxSdk(): void
{
$phpxDir = $this->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);
}
};
}

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

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

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

@ -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) {

@ -0,0 +1,111 @@
<?php
namespace TypePhp\Platform;
/**
* iPhoneOS cross-compilation target hosted on macOS.
*
* iOS executables link PHP and PHPX statically and must not inherit Homebrew
* include paths, library paths, or runtime rpaths from the macOS host.
*/
class Ios extends UnixPlatform
{
public static function supportsTarget(string $target): bool
{
return preg_match('/^(?:arm64|aarch64)-apple-ios(?:\d+(?:\.\d+)*)?$/i', $target) === 1;
}
public function getName(): string
{
return 'iOS';
}
public function isCurrent(): bool
{
return false;
}
public function getSharedLibraryExtension(): string
{
return '.a';
}
public function getDefaultCompiler(): string
{
return 'xcrun --sdk iphoneos clang++';
}
public function getDefaultRpaths(?string $phpxDir = null, ?string $phpDir = null): array
{
return [];
}
/**
* An iPhoneOS SDK is a target sysroot, not a runnable PHP installation.
* Its path is resolved from PHPX_HOME by CompilerBase, and php-config is
* never executed for the target.
*/
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 iPhoneOS libphp.a was not found: {$archive}");
}
return [
'embed' => 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;
}
}

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

Loading…
Cancel
Save