- 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 updatesmaster
parent
622d6b0a00
commit
b356636853
20 changed files with 782 additions and 66 deletions
@ -1,5 +1,7 @@ |
|||||||
/build/ |
/build/ |
||||||
/build-*/ |
/build-*/ |
||||||
|
/ios-build/ |
||||||
/dist/ |
/dist/ |
||||||
/*.rsp |
/*.rsp |
||||||
/typephp_macos_hello |
/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> |
||||||
@ -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 |
||||||
@ -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'); |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue