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.
 
 

315 lines
12 KiB

<?php
use TypePhp\Translator;
use TypePhp\Build\WasiToolchain;
use TypePhp\Build\WasiProjectConfig;
use TypePhp\Build\PhpxLocator;
function main(int $argc, array $argv): 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');
if (!defined('ROOT_PATH')) {
define("ROOT_PATH", getcwd());
}
if (getenv('TYPEPHP_WASM_INTERNAL_COMPILE') !== '1' && shouldCompileWasm($argv)) {
compileWasmProgram($argv);
return;
}
// .prof 文件分析模式:./tpc app.prof
if ($argc >= 2 && str_ends_with($argv[1], '.prof')) {
profileAnalyze($argc, $argv);
return;
}
require_once ROOT_PATH . '/vendor/autoload.php';
global $translator;
$translator = new Translator(ROOT_PATH);
$translator->setIndent(' ');
// 扫描所有 PHP 文件,预处理
$files = $translator->prepare($translator->parseArgv($argv));
// 生成 C++ 文件
$sourceFiles = $translator->convert($files);
$wasmManifest = getenv('TYPEPHP_WASM_INTERFACE_MANIFEST');
if (is_string($wasmManifest) && $wasmManifest !== '') {
$wasmWit = getenv('TYPEPHP_WASM_INTERFACE_WIT');
$wasmAdapter = getenv('TYPEPHP_WASM_INTERFACE_ADAPTER');
$wasmAsyncExports = getenv('TYPEPHP_WASM_INTERFACE_ASYNC_EXPORTS');
$wasmPackage = getenv('TYPEPHP_WASM_PACKAGE');
$wasmWorld = getenv('TYPEPHP_WASM_WORLD');
if (!is_string($wasmWit) || $wasmWit === ''
|| !is_string($wasmAdapter) || $wasmAdapter === ''
|| !is_string($wasmAsyncExports) || $wasmAsyncExports === ''
|| !is_string($wasmPackage) || $wasmPackage === ''
|| !is_string($wasmWorld) || $wasmWorld === '') {
throw new RuntimeException('Incomplete internal WASM interface configuration');
}
$translator->writeWasmInterface(
$wasmManifest,
$wasmWit,
$wasmAdapter,
$wasmAsyncExports,
$wasmPackage,
$wasmWorld,
);
$sourceFiles[] = $wasmAdapter;
}
// --dry 模式:仅生成 C++ 代码,不执行编译
if ($translator->isDryRun()) {
$buildDir = $translator->getBuildDir();
$count = count($sourceFiles);
$sourceListFile = getenv('TYPEPHP_GENERATED_SOURCE_LIST');
if (is_string($sourceListFile) && $sourceListFile !== '') {
$sourceListDir = dirname($sourceListFile);
if (!is_dir($sourceListDir) && !mkdir($sourceListDir, 0777, true) && !is_dir($sourceListDir)) {
throw new RuntimeException("Unable to create generated source manifest directory: {$sourceListDir}");
}
if (file_put_contents($sourceListFile, implode(PHP_EOL, $sourceFiles) . PHP_EOL) === false) {
throw new RuntimeException("Unable to write generated source manifest: {$sourceListFile}");
}
}
$translator->output("Dry run completed: {$count} C++ source file(s) generated in {$buildDir}", 'lightBlue');
return;
}
// 编译所有 C++ 文件
$objectFiles = $translator->compile($sourceFiles);
// 连接所有目标文件,生成可执行文件
$binaryFile = $translator->build($objectFiles);
// 如果指定了 --run / -r,编译完成后立即执行
if ($translator->isRunRequested()) {
$translator->run($binaryFile); // never returns
}
}
/**
* 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): void
{
$input = null;
$buildDir = null;
$profile = null;
$arguments = array_slice($argv, 1);
for ($i = 0, $count = count($arguments); $i < $count; $i++) {
$argument = $arguments[$i];
if ($argument === '--wasm') {
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,
ROOT_PATH . DIRECTORY_SEPARATOR . 'build',
$profile,
);
} catch (RuntimeException $exception) {
fwrite(STDERR, "Invalid WASI project: {$exception->getMessage()}\n");
exit(1);
}
$builder = dirname(__DIR__) . '/wasm/build-typephp-program.sh';
if (!is_executable($builder)) {
fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n");
exit(1);
}
try {
$tools = (new WasiToolchain())->detect($project->profile === 'browser');
} 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");
}
fwrite(STDERR, ", then try again.\n");
exit(1);
}
$environment = getenv();
if (!is_array($environment)) {
$environment = [];
}
$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;
$compilerExecutable = realpath($argv[0]);
if ($compilerExecutable === false || !is_executable($compilerExecutable)) {
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(ROOT_PATH);
} catch (RuntimeException $exception) {
fwrite(STDERR, "Unable to locate PHPX: {$exception->getMessage()}\n");
exit(1);
}
if ($project->mode === 'library') {
$hostOs = match (PHP_OS_FAMILY) {
'Linux' => 'linux',
'Darwin' => 'macos',
'Windows' => 'windows',
default => strtolower(PHP_OS_FAMILY),
};
$hostArch = strtolower(php_uname('m'));
$hostArch = match ($hostArch) {
'amd64', 'x64' => 'x86_64',
'arm64' => $hostOs === 'linux' ? 'aarch64' : 'arm64',
default => $hostArch,
};
$bindgen = $phpxDir . DIRECTORY_SEPARATOR . 'wasm' . DIRECTORY_SEPARATOR . 'bin'
. DIRECTORY_SEPARATOR . $hostOs . '-' . $hostArch . DIRECTORY_SEPARATOR . 'wit-bindgen'
. ($hostOs === 'windows' ? '.exe' : '');
if (!is_file($bindgen)) {
fwrite(STDERR, "PHPX bundled WIT binding generator is missing: {$bindgen}\n");
fwrite(STDERR, "Install the matching PHPX package; installing wit-bindgen separately is not required.\n");
exit(1);
}
$environment['TYPEPHP_WIT_BINDGEN'] = $bindgen;
}
$command = [$builder, $project->input, $project->output ?? '-', $phpxDir, $compilerExecutable];
$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();
foreach (array_slice($argv, 1) as $argument) {
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);
}
// 从 prof 文件名推导二进制文件名(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);
}