diff --git a/README.md b/README.md index f268bb3d..99d2b0de 100644 --- a/README.md +++ b/README.md @@ -257,17 +257,22 @@ string(16) "Linux ..." > arguments, and must return `void`. Top-level executable statements are not > allowed; executable code belongs in a function or method. -### VM-free Nano executable +### VM-free Nano executables and libraries -Use `--nano` to compile one PHP source file together with PHP Nano and PHPX -sources. The result does not link `libphp` and contains no Zend opcode -interpreter: +Use `--nano` to compile PHP sources together with PHP Nano and PHPX sources. +The result does not link `libphp` and contains no Zend opcode interpreter: ```bash ./bin/tpc.php --nano examples/hello.php ./hello ``` +Nano supports `mode: bin` and `mode: lib`. Binary mode generates the process +`main()` entry and requires a TypePHP `main()` function. Library mode defines +`TYPEPHP_NO_MAIN` and exports the project-specific runtime initialization and +shutdown ABI for hosts such as an Android Activity. Nano does not support +`mode: ext`. + By default, the executable is emitted in the directory where `tpc` was invoked. Normal and Nano builds share the `build` directory for generated code, objects, and other intermediate files. Use `-o` to select a different output path. diff --git a/examples/android-native/README.md b/examples/android-native/README.md index d5595c14..a664fcd2 100644 --- a/examples/android-native/README.md +++ b/examples/android-native/README.md @@ -11,17 +11,24 @@ Prerequisites: - Android SDK Platform/Build Tools 36 - Android NDK r27 or newer - JDK with `javac` and `keytool` -- the PHPX Android SDK produced by `phpx/sdk/build-native.sh` +- the compiler Composer dependencies, including php-nano and PHPX sources Build: ```sh export ANDROID_SDK_ROOT=/home/swoole/soft/android-sdk export ANDROID_NDK_HOME=/home/swoole/soft/android-ndk-r27d -export PHPX_ANDROID_SDK_DIR=/path/to/android-arm64-v8a-sdk -./build-app.sh +export PHPX_HOME=/path/to/phpx +./build-app.sh --nano ``` +`tpc.php` itself still runs dynamically on the host PHP. `--nano` applies to +the generated JNI shared library, which compiles the selected php-nano and PHPX +sources directly and does not require a prebuilt Android `libphp.a` or +`libphpx.a`. For a non-Nano build, additionally set +`PHPX_ANDROID_SDK_DIR` to the SDK produced by `phpx/sdk/build-native.sh` and run +`./build-app.sh` without `--nano`. + The result is `dist/typephp-android-hello-debug.apk`. With an arm64 Android device connected and USB debugging enabled, install and launch it using: diff --git a/examples/android-native/build-app.sh b/examples/android-native/build-app.sh index 872892fe..a78e3cfc 100755 --- a/examples/android-native/build-app.sh +++ b/examples/android-native/build-app.sh @@ -8,6 +8,16 @@ android_sdk=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/home/swoole/soft/android-sdk}} android_ndk=${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-/home/swoole/soft/android-ndk-r27d}} phpx_root=${PHPX_HOME:-$(cd "${compiler_root}/../phpx" && pwd)} phpx_android_sdk=${PHPX_ANDROID_SDK_DIR:-${phpx_root}/android/arm64-v8a} +nano_arg= + +case "${1:-}" in + "") ;; + --nano) nano_arg=--nano ;; + *) + echo "Usage: $0 [--nano]" >&2 + exit 2 + ;; +esac if [[ ! -d "${android_sdk}/build-tools" || ! -f "${android_sdk}/platforms/android-36/android.jar" ]]; then echo "Android SDK Platform 36 and Build Tools are required: ${android_sdk}" >&2 @@ -17,7 +27,7 @@ if [[ ! -f "${android_ndk}/build/cmake/android.toolchain.cmake" ]]; then echo "Android NDK is required: ${android_ndk}" >&2 exit 1 fi -if [[ ! -f "${phpx_android_sdk}/.typephp-android-sdk-abi" ]]; then +if [[ -z "${nano_arg}" && ! -f "${phpx_android_sdk}/.typephp-android-sdk-abi" ]]; then echo "TypePHP Android SDK is required: ${phpx_android_sdk}" >&2 exit 1 fi @@ -39,13 +49,19 @@ mkdir -p \ "${resources_dir}/drawable-nodpi" \ "${compiled_resources_dir}" \ "${dist_dir}" -cp -p "${compiler_root}/examples/objective-c-macos/ios-assets/AppIcon-180.png" \ +cp -p "${compiler_root}/examples/apple-native/ios-assets/AppIcon-180.png" \ "${resources_dir}/drawable-nodpi/typephp_icon.png" -ANDROID_NDK_HOME="${android_ndk}" \ -PHPX_HOME="${phpx_root}" \ -PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \ -php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress +if [[ -n "${nano_arg}" ]]; then + ANDROID_NDK_HOME="${android_ndk}" \ + PHPX_HOME="${phpx_root}" \ + php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" "${nano_arg}" --no-progress +else + ANDROID_NDK_HOME="${android_ndk}" \ + PHPX_HOME="${phpx_root}" \ + PHPX_ANDROID_SDK_DIR="${phpx_android_sdk}" \ + php "${compiler_root}/bin/tpc.php" "${project_dir}/android.yml" --no-progress +fi javac -source 8 -target 8 -encoding UTF-8 \ -bootclasspath "${android_jar}" \ diff --git a/phpunit/src/Build/NativeBuildConfigurationTest.php b/phpunit/src/Build/NativeBuildConfigurationTest.php index 8fb3d3c1..9e2eefbc 100644 --- a/phpunit/src/Build/NativeBuildConfigurationTest.php +++ b/phpunit/src/Build/NativeBuildConfigurationTest.php @@ -183,6 +183,20 @@ final class NativeBuildConfigurationTest extends TestCase self::assertSame(24, Android::getApiLevel('aarch64-linux-android24')); } + public function testAndroidNanoLibraryDoesNotRequirePrebuiltPhpSdk(): void + { + $compiler = $this->newCompiler(new Android()); + $compiler->setBuildMode(\TypePhp\CompilerBase::BUILD_MODE_LIB); + $compiler->enableNanoForTest(); + + self::assertSame([], $compiler->getLibraryPathsForTest()); + self::assertSame([], $compiler->getLibrariesForTest()); + $defines = $compiler->getCommonCompileOptionsForTest()['user_defines']; + self::assertContains('PHPX_ANDROID=1', $defines); + self::assertContains('TYPEPHP_NO_MAIN=1', $defines); + self::assertContains('PHP_NANO=1', $defines); + } + public function testNativeModulesDoNotFallBackToStaticPhpx(): void { $phpxDir = $this->temporaryDirectory('phpx-static-module'); diff --git a/phpunit/src/Build/NativeDependencyAuditorTest.php b/phpunit/src/Build/NativeDependencyAuditorTest.php index bc151328..2280c3bb 100644 --- a/phpunit/src/Build/NativeDependencyAuditorTest.php +++ b/phpunit/src/Build/NativeDependencyAuditorTest.php @@ -79,6 +79,23 @@ final class NativeDependencyAuditorTest extends TestCase self::addToAssertionCount(1); } + public function testAndroidAllowsRequiredBionicAbiButStillRejectsNetwork(): void + { + $auditor = new NativeDependencyAuditor(); + $auditor->assertUndefinedSymbols( + 'android', + " U closelog\n" + . " U openlog\n" + . " U syscall\n" + . " U syslog\n", + ); + self::addToAssertionCount(1); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('socket'); + $auditor->assertUndefinedSymbols('android', " U socket\n"); + } + public function testOnlyNonPosixExceptionsNeedAnAllowlistEntry(): void { self::assertContains('flock', NativeDependencyAuditor::NON_POSIX_HOST_FUNCTION_ALLOWLIST); diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 7c1befad..346d70fa 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -1334,6 +1334,22 @@ YAML); $this->assertSame([dirname($projectFile) . '/build'], $this->compiler->getLinkPaths()); } + public function testNanoModeSupportsLibraryBuildsButRejectsExtensions(): void + { + $this->setPropertyValue('nanoMode', true); + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $this->invokeMethod('applyCommandLineArguments'); + + $this->assertSame(CompilerBase::BUILD_MODE_LIB, $this->compiler->getBuildMode()); + + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_EXT); + $this->expectException(TestError::class); + $this->expectExceptionMessage( + '--nano source composition does not support extension mode (-m ext)', + ); + $this->invokeMethod('applyCommandLineArguments'); + } + public function testParseProjectYamlFiltersIgnoredFilesFromReturnedSources(): void { $projectFile = $this->createProjectFile(<<<'YAML' @@ -1686,6 +1702,32 @@ YAML); ); } + public function testNanoLibraryRuntimeEntryHasProjectSpecificCompileOptions(): void + { + $this->compiler->setTargetName('nano_module_accessor'); + $this->setPropertyValue('buildMode', CompilerBase::BUILD_MODE_LIB); + $this->setPropertyValue('nanoMode', true); + $this->setPropertyValue('precompiledHeader', [ + 'header' => '/tmp/typephp_pch.hpp', + 'artifact' => '/tmp/typephp_pch.hpp.gch', + ]); + + $phpxDir = $this->invokeMethod('getPhpxDir'); + $entry = $phpxDir . '/src/typephp/typephp_main_nano.cc'; + $options = $this->invokeMethod('getSourceCompileCommandOptions', $entry, null); + + $this->assertContains('TYPEPHP_PROJECT_NAME=nano_module_accessor', $options['user_defines']); + $this->assertContains('TYPEPHP_RUNTIME_EXPORTS=1', $options['user_defines']); + $this->assertArrayNotHasKey('forced_include', $options->toArray()); + $this->assertArrayNotHasKey('precompiled_header', $options->toArray()); + $this->assertFalse($this->compiler->hasMiscObjectFileCache($entry)); + + $nanoCore = dirname($phpxDir) . '/php-nano/src/core.cpp'; + $this->setPropertyValue('nanoRuntimeSources', [$nanoCore => true]); + $coreOptions = $this->invokeMethod('getSourceCompileCommandOptions', $nanoCore, null); + $this->assertArrayNotHasKey('forced_include', $coreOptions->toArray()); + } + public function testProjectIndependentMiscObjectsUseSharedCacheScope(): void { $phpxDir = $this->invokeMethod('getPhpxDir'); diff --git a/src/Build/NativeDependencyAuditor.php b/src/Build/NativeDependencyAuditor.php index 9fa5ee95..71d0d0ee 100644 --- a/src/Build/NativeDependencyAuditor.php +++ b/src/Build/NativeDependencyAuditor.php @@ -15,6 +15,14 @@ final class NativeDependencyAuditor 'flock', ]; + /** Bionic/libc++ implementation imports required by an Android JNI library. */ + public const array ANDROID_SYSTEM_ABI_ALLOWLIST = [ + 'closelog', + 'openlog', + 'syscall', + 'syslog', + ]; + /** @param list $flags */ public function assertLinkFlags(string $target, array $flags): void { @@ -79,6 +87,10 @@ final class NativeDependencyAuditor if ($symbol === 'proc_raise' || $symbol === 'sched_yield') { return true; } + } elseif ($target === 'android' + && in_array($symbol, self::ANDROID_SYSTEM_ABI_ALLOWLIST, true) + ) { + return false; } elseif (in_array($symbol, self::NON_POSIX_HOST_FUNCTION_ALLOWLIST, true)) { return false; } diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index 51d95dab..656891fc 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -1228,7 +1228,7 @@ trait SourcePipelineTrait && ($this->isSapiBuild() || $this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== [])) { array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable()); } - if ($this->isNanoMode()) { + if ($this->isNanoMode() && $this->isBuildModeBin()) { $sourceFiles[] = $this->genNanoEntrypoint(); } $this->getStableIdRegistry()->flush(); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index cfafe630..37a04693 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -434,7 +434,18 @@ class Preprocessor extends CompilerBase { $normalizedFile = str_replace('\\', '/', $file); $runtimeEntry = str_replace('\\', '/', $this->getPhpxDir() . '/src/misc/typephp_main.cc'); - return $normalizedFile === $runtimeEntry; + if ($normalizedFile === $runtimeEntry) { + return true; + } + if (!$this->isNanoMode() || !$this->isBuildModeLib()) { + return false; + } + $nanoLibraryEntry = str_replace( + '\\', + '/', + $this->getPhpxDir() . '/src/typephp/typephp_main_nano.cc', + ); + return $normalizedFile === $nanoLibraryEntry; } public function prepareFile(string $file): void diff --git a/src/Translator.php b/src/Translator.php index 497bcc28..b2058a8f 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -653,8 +653,8 @@ class Translator extends Preprocessor } if ($this->isNanoMode()) { - if (!$this->isBuildModeBin()) { - $this->error('--nano source composition only supports binary mode (-m bin)'); + if ($this->isBuildModeExt()) { + $this->error('--nano source composition does not support extension mode (-m ext)'); } if ($this->cxxStd !== 'c++17') { $this->error('--nano requires the C++17 language standard'); @@ -2493,7 +2493,9 @@ CODE; 'c' => $this->getCCompileCommandOptions(), default => $this->getNativeCompileCommandOptions($language), }; - if ($this->isGeneratedProjectSource($sourceFile) && isset($options['forced_include'])) { + if (($this->isGeneratedProjectSource($sourceFile) + || isset($this->nanoRuntimeSources[$sourceFile])) + && isset($options['forced_include'])) { $values = $options->toArray(); unset($values['forced_include']); return new CompileOptions($values); @@ -3042,6 +3044,19 @@ CODE; ); return; } + if ($this->isAndroidTarget()) { + $nm = getenv('TYPEPHP_ANDROID_NM'); + if (!is_string($nm) || $nm === '') { + $compiler = $this->getCompilerBackend()->getCompilerCommand(); + $candidate = dirname($compiler) . DIRECTORY_SEPARATOR . 'llvm-nm'; + $nm = is_executable($candidate) ? $candidate : 'llvm-nm'; + } + $auditor->assertUndefinedSymbols( + 'android', + $this->captureNativeCommand([$nm, '--undefined-only', $targetFile]), + ); + return; + } /* Native objects may deliberately provide an OS ABI to one another * (for example a freestanding syscall shim). Only unresolved imports * in the final ELF cross the host-capability boundary. */