TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
16 KiB
459 lines
16 KiB
<?php
|
|
use TypePhp\Translator;
|
|
use TypePhp\Build\WasiToolchain;
|
|
use TypePhp\Build\WasiProjectConfig;
|
|
use TypePhp\Build\PhpxLocator;
|
|
use TypePhp\Build\CompilerRuntime;
|
|
use TypePhp\Build\NativeSourceProjectBuilder;
|
|
use TypePhp\Build\NativeSourceProjectConfig;
|
|
use TypePhp\Build\ProjectBuildRunner;
|
|
use TypePhp\PythonTools\Command as PythonToolsCommand;
|
|
use TypePhp\Cli\CompletionCommand;
|
|
use TypePhp\Resolver\Reflection as ReflectionResolver;
|
|
|
|
function main(int $argc, array $argv): void
|
|
{
|
|
runCompiler($argc, $argv, CompilerRuntime::native($argv[0]));
|
|
}
|
|
|
|
function runCompiler(int $argc, array $argv, CompilerRuntime $runtime): void
|
|
{
|
|
// Compiling a complete project keeps the parsed AST and generated sources in
|
|
// memory. The default CLI limit (commonly 128M) is too small for larger builds.
|
|
ini_set('memory_limit', '-1');
|
|
|
|
// The late-loaded embed module is unloaded before PHP's object store. Drop
|
|
// cached reflector wrappers while their referenced internal symbols are
|
|
// still alive. Embed serves one request per compiler process; never add
|
|
// this traversal to an extension/CLI/FPM request-shutdown path.
|
|
if (php_sapi_name() === 'embed') {
|
|
register_shutdown_function([ReflectionResolver::class, 'clearCaches']);
|
|
}
|
|
|
|
// The PHP entrypoint already loaded Composer's project autoloader in
|
|
// bin/bootstrap.php. The native binary loads its embedded copy here.
|
|
if (!$runtime->sourceEntry) {
|
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
|
}
|
|
|
|
configureProxyEnvironment($argv);
|
|
|
|
$completionStatus = CompletionCommand::execute($argv);
|
|
if ($completionStatus !== null) {
|
|
if ($completionStatus !== 0) {
|
|
exit($completionStatus);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$pythonToolStatus = PythonToolsCommand::execute($argv);
|
|
if ($pythonToolStatus !== null) {
|
|
if ($pythonToolStatus !== 0) {
|
|
exit($pythonToolStatus);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$nativeSourceProject = shouldCompileNativeSourceProject($argv);
|
|
} catch (RuntimeException $exception) {
|
|
fwrite(STDERR, 'Native source build failed: ' . $exception->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
if ($nativeSourceProject) {
|
|
compileNativeSourceProject($argv, $runtime);
|
|
return;
|
|
}
|
|
|
|
if (getenv('TYPEPHP_WASM_INTERNAL_COMPILE') !== '1' && shouldCompileWasm($argv)) {
|
|
compileWasmProgram($argv, $runtime);
|
|
return;
|
|
}
|
|
|
|
// .prof file analysis mode: ./tpc app.prof
|
|
if ($argc >= 2 && str_ends_with($argv[1], '.prof')) {
|
|
profileAnalyze($argc, $argv);
|
|
return;
|
|
}
|
|
|
|
(new ProjectBuildRunner(Translator::getInstance($runtime)))->run($argv);
|
|
}
|
|
|
|
/**
|
|
* Export an explicit CLI proxy for every network-capable child process. The
|
|
* downloader still receives the value directly so credentials can be redacted
|
|
* from its diagnostics, while tools such as package managers inherit the
|
|
* conventional proxy variables from the compiler process.
|
|
*/
|
|
function configureProxyEnvironment(array $argv): void
|
|
{
|
|
$proxy = null;
|
|
for ($i = 1, $count = count($argv); $i < $count; ++$i) {
|
|
$argument = $argv[$i];
|
|
if ($argument === '--proxy') {
|
|
if (!isset($argv[$i + 1]) || trim($argv[$i + 1]) === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
$proxy = trim($argv[++$i]);
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--proxy=')) {
|
|
$proxy = trim(substr($argument, strlen('--proxy=')));
|
|
if ($proxy === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($proxy === null) {
|
|
return;
|
|
}
|
|
foreach (['HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'all_proxy'] as $name) {
|
|
putenv($name . '=' . $proxy);
|
|
$_ENV[$name] = $proxy;
|
|
}
|
|
}
|
|
|
|
function shouldCompileNativeSourceProject(array $argv): bool
|
|
{
|
|
$arguments = array_slice($argv, 1);
|
|
for ($i = 0, $count = count($arguments); $i < $count; ++$i) {
|
|
$argument = $arguments[$i];
|
|
if ($argument === '--proxy') {
|
|
++$i;
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--proxy=')) {
|
|
continue;
|
|
}
|
|
if ($argument === '' || $argument[0] === '-') {
|
|
continue;
|
|
}
|
|
$path = $argument;
|
|
if ($path[0] !== '/' && preg_match('/^[A-Za-z]:[\\\\\/]/', $path) !== 1) {
|
|
$path = getcwd() . DIRECTORY_SEPARATOR . $path;
|
|
}
|
|
return NativeSourceProjectConfig::isNativeProject($path);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function compileNativeSourceProject(array $argv, CompilerRuntime $runtime): void
|
|
{
|
|
$input = null;
|
|
$buildDir = null;
|
|
$run = false;
|
|
$arguments = array_slice($argv, 1);
|
|
for ($i = 0, $count = count($arguments); $i < $count; ++$i) {
|
|
$argument = $arguments[$i];
|
|
if ($argument === '--run' || $argument === '-r') {
|
|
$run = true;
|
|
continue;
|
|
}
|
|
if ($argument === '--proxy') {
|
|
if (!isset($arguments[$i + 1]) || $arguments[$i + 1] === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
++$i;
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--proxy=')) {
|
|
if (substr($argument, strlen('--proxy=')) === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
continue;
|
|
}
|
|
if ($argument === '--build-dir') {
|
|
if (!isset($arguments[$i + 1]) || $arguments[$i + 1] === '') {
|
|
fwrite(STDERR, "Option --build-dir requires a directory\n");
|
|
exit(1);
|
|
}
|
|
$buildDir = $arguments[++$i];
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--build-dir=')) {
|
|
$buildDir = substr($argument, strlen('--build-dir='));
|
|
if ($buildDir === '') {
|
|
fwrite(STDERR, "Option --build-dir requires a directory\n");
|
|
exit(1);
|
|
}
|
|
continue;
|
|
}
|
|
if ($argument !== '' && $argument[0] === '-') {
|
|
fwrite(STDERR, "Unsupported native option: {$argument}\n");
|
|
exit(1);
|
|
}
|
|
if ($input !== null) {
|
|
fwrite(STDERR, "Native mode accepts exactly one project.xml\n");
|
|
exit(1);
|
|
}
|
|
$input = $argument;
|
|
}
|
|
|
|
if ($input === null) {
|
|
fwrite(STDERR, "Usage: vendor/bin/tpc project.xml [--build-dir DIR] [--run]\n");
|
|
exit(1);
|
|
}
|
|
|
|
try {
|
|
$project = NativeSourceProjectConfig::load($input, $buildDir);
|
|
$builder = new NativeSourceProjectBuilder(compilerRuntime: $runtime);
|
|
$result = $builder->build($project);
|
|
fwrite(
|
|
STDOUT,
|
|
"Native source build completed: {$result['sourceCount']} source file(s), "
|
|
. "{$result['compiledCount']} compiled, "
|
|
. "output {$result['output']}\n"
|
|
);
|
|
if ($run) {
|
|
$builder->runOutput($project);
|
|
}
|
|
} catch (RuntimeException $exception) {
|
|
fwrite(STDERR, "Native source build failed: {$exception->getMessage()}\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a self-contained WASI 0.2 command component through the public CLI.
|
|
* The lower-level build scripts are implementation details and are not part of
|
|
* the user-facing workflow.
|
|
*/
|
|
function compileWasmProgram(array $argv, CompilerRuntime $runtime): void
|
|
{
|
|
$input = null;
|
|
$buildDir = null;
|
|
$profile = null;
|
|
$nano = false;
|
|
$arguments = array_slice($argv, 1);
|
|
for ($i = 0, $count = count($arguments); $i < $count; $i++) {
|
|
$argument = $arguments[$i];
|
|
if ($argument === '--wasm') {
|
|
continue;
|
|
}
|
|
if ($argument === '--nano') {
|
|
$nano = true;
|
|
continue;
|
|
}
|
|
if ($argument === '--proxy') {
|
|
if (!isset($arguments[$i + 1]) || $arguments[$i + 1] === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
++$i;
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--proxy=')) {
|
|
if (substr($argument, strlen('--proxy=')) === '') {
|
|
fwrite(STDERR, "Option --proxy requires a URL\n");
|
|
exit(1);
|
|
}
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--wasm=')) {
|
|
$value = substr($argument, strlen('--wasm='));
|
|
if ($value === '') {
|
|
fwrite(STDERR, "Option --wasm requires browser or component after `=`\n");
|
|
exit(1);
|
|
}
|
|
if ($profile !== null && $profile !== $value) {
|
|
fwrite(STDERR, "Option --wasm was specified with conflicting profiles\n");
|
|
exit(1);
|
|
}
|
|
$profile = $value;
|
|
continue;
|
|
}
|
|
if ($argument === '--build-dir') {
|
|
if (!isset($arguments[$i + 1]) || $arguments[$i + 1] === '') {
|
|
fwrite(STDERR, "Option --build-dir requires a directory\n");
|
|
exit(1);
|
|
}
|
|
$buildDir = $arguments[++$i];
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--build-dir=')) {
|
|
$buildDir = substr($argument, strlen('--build-dir='));
|
|
if ($buildDir === '') {
|
|
fwrite(STDERR, "Option --build-dir requires a directory\n");
|
|
exit(1);
|
|
}
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '-')) {
|
|
fwrite(STDERR, "Unsupported option in --wasm mode: {$argument}\n");
|
|
exit(1);
|
|
}
|
|
if ($input !== null) {
|
|
fwrite(STDERR, "The --wasm mode accepts exactly one PHP file or project.yml\n");
|
|
exit(1);
|
|
}
|
|
$input = $argument;
|
|
}
|
|
|
|
if ($input === null) {
|
|
fwrite(STDERR, "Usage: php bin/tpc.php <program.php|project.yml> [--wasm[=browser|component]] [--build-dir <directory>]\n");
|
|
exit(1);
|
|
}
|
|
|
|
$workingDirectory = getcwd();
|
|
try {
|
|
$project = WasiProjectConfig::load(
|
|
$input,
|
|
$buildDir,
|
|
$workingDirectory,
|
|
$runtime->installationRoot . DIRECTORY_SEPARATOR . 'build',
|
|
$profile,
|
|
);
|
|
} catch (RuntimeException $exception) {
|
|
fwrite(STDERR, "Invalid WASI project: {$exception->getMessage()}\n");
|
|
exit(1);
|
|
}
|
|
|
|
$builder = $runtime->installationRoot . '/wasm/'
|
|
. ($nano ? 'build-nano-program.sh' : 'build-program.sh');
|
|
if (!is_executable($builder)) {
|
|
fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n");
|
|
exit(1);
|
|
}
|
|
|
|
try {
|
|
$tools = (new WasiToolchain())->detect(
|
|
requireBrowserTools: $project->profile === 'browser',
|
|
requireWitBindgen: $project->mode === 'library',
|
|
);
|
|
} catch (RuntimeException $exception) {
|
|
fwrite(STDERR, "WASI toolchain check failed: {$exception->getMessage()}\n");
|
|
fwrite(STDERR, "Add WASI SDK and Wasmtime bin directories to PATH");
|
|
if ($project->profile === 'browser') {
|
|
fwrite(STDERR, ", and install Jco (`npm install -g @bytecodealliance/jco`)"
|
|
. " or use --wasm=component");
|
|
}
|
|
if ($project->mode === 'library') {
|
|
fwrite(STDERR, ", and install wit-bindgen-cli 0.60.0"
|
|
. " (`cargo install wit-bindgen-cli --version 0.60.0 --locked`)");
|
|
}
|
|
fwrite(STDERR, ", then try again.\n");
|
|
exit(1);
|
|
}
|
|
|
|
$environment = getenv();
|
|
$environment['TYPEPHP_WASI_CC'] = $tools['clang'];
|
|
$environment['TYPEPHP_WASI_CXX'] = $tools['clang++'];
|
|
$environment['TYPEPHP_WASI_AR'] = $tools['llvm-ar'];
|
|
$environment['TYPEPHP_WASI_RANLIB'] = $tools['llvm-ranlib'];
|
|
$environment['TYPEPHP_WASI_NM'] = $tools['llvm-nm'];
|
|
$environment['TYPEPHP_WASI_LD'] = $tools['wasm-ld'];
|
|
$environment['TYPEPHP_WASMTIME'] = $tools['wasmtime'];
|
|
$environment['TYPEPHP_WASM_BROWSER'] = $project->profile === 'browser' ? '1' : '0';
|
|
if ($project->profile === 'browser') {
|
|
$environment['TYPEPHP_JCO'] = $tools['jco'];
|
|
$environment['TYPEPHP_JCO_VERSION'] = $tools['jco-version'];
|
|
}
|
|
$environment['TYPEPHP_WASI_TARGET'] = $tools['target'];
|
|
$environment['TYPEPHP_WASI_CLANG_VERSION'] = $tools['clang-version'];
|
|
$environment['TYPEPHP_WASMTIME_VERSION'] = $tools['wasmtime-version'];
|
|
$environment['TYPEPHP_WASM_PROGRAM_BUILD_DIR'] = $project->buildDir;
|
|
$environment['TYPEPHP_WASM_MODE'] = $project->mode;
|
|
$environment['TYPEPHP_WASM_PACKAGE'] = $project->package;
|
|
$environment['TYPEPHP_WASM_WORLD'] = $project->world;
|
|
$environment['TYPEPHP_WASM_NANO'] = $nano ? '1' : '0';
|
|
if (!is_file($runtime->executable) || !is_executable($runtime->executable)) {
|
|
fwrite(STDERR, "Unable to resolve the current TypePHP compiler executable: {$argv[0]}\n");
|
|
exit(1);
|
|
}
|
|
if ($project->browserDir !== null) {
|
|
$environment['TYPEPHP_WASM_BROWSER_DIR'] = $project->browserDir;
|
|
}
|
|
|
|
try {
|
|
$phpxDir = PhpxLocator::resolve($runtime->installationRoot);
|
|
} catch (RuntimeException $exception) {
|
|
fwrite(STDERR, "Unable to locate PHPX: {$exception->getMessage()}\n");
|
|
exit(1);
|
|
}
|
|
if ($project->mode === 'library') {
|
|
$environment['TYPEPHP_WIT_BINDGEN'] = $tools['wit-bindgen'];
|
|
}
|
|
$command = [$builder, $project->input, $project->output ?? '-', $phpxDir, $runtime->executable];
|
|
|
|
$process = proc_open(
|
|
$command,
|
|
[STDIN, STDOUT, STDERR],
|
|
$pipes,
|
|
getcwd(),
|
|
$environment,
|
|
);
|
|
if (!is_resource($process)) {
|
|
fwrite(STDERR, "Failed to start the TypePHP WASI builder\n");
|
|
exit(1);
|
|
}
|
|
|
|
$exitCode = proc_close($process);
|
|
if ($exitCode !== 0) {
|
|
exit($exitCode);
|
|
}
|
|
}
|
|
|
|
function shouldCompileWasm(array $argv): bool
|
|
{
|
|
foreach (array_slice($argv, 1) as $argument) {
|
|
if ($argument === '--wasm' || str_starts_with($argument, '--wasm=')) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$workingDirectory = getcwd();
|
|
$arguments = array_slice($argv, 1);
|
|
for ($i = 0, $count = count($arguments); $i < $count; ++$i) {
|
|
$argument = $arguments[$i];
|
|
if ($argument === '--proxy') {
|
|
++$i;
|
|
continue;
|
|
}
|
|
if (str_starts_with($argument, '--proxy=')) {
|
|
continue;
|
|
}
|
|
if ($argument === '' || $argument[0] === '-') {
|
|
continue;
|
|
}
|
|
$path = $argument;
|
|
if ($path[0] !== '/' && preg_match('/^[A-Za-z]:[\\\\\/]/', $path) !== 1) {
|
|
$path = $workingDirectory . DIRECTORY_SEPARATOR . $path;
|
|
}
|
|
if (WasiProjectConfig::isWasmEnabled($path)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function profileAnalyze(int $argc, array $argv): void
|
|
{
|
|
$profFile = $argv[1];
|
|
|
|
if (!file_exists($profFile)) {
|
|
fwrite(STDERR, "Profile file not found: {$profFile}\n");
|
|
exit(1);
|
|
}
|
|
|
|
// Derive the binary name from the prof file name (app.prof → app).
|
|
$binary = basename($profFile, '.prof');
|
|
if (!file_exists($binary) && file_exists('./' . $binary)) {
|
|
$binary = './' . $binary;
|
|
}
|
|
|
|
if (!file_exists($binary)) {
|
|
fwrite(STDERR, "Binary not found: {$binary} (expected from prof file name)\n");
|
|
fwrite(STDERR, "Usage: ./tpc <binary>.prof\n");
|
|
exit(1);
|
|
}
|
|
|
|
$cmd = 'pprof --web ' . escapeshellarg($binary) . ' ' . escapeshellarg($profFile);
|
|
fwrite(STDERR, "Running: {$cmd}\n");
|
|
passthru($cmd, $exitCode);
|
|
exit($exitCode);
|
|
}
|
|
|