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
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);
|
|
}
|
|
|