feat(cli): add compiler option and improve completion for tpc

- Added --compiler option to specify C++ compiler command
- Enhanced bash completion to suggest commands for --compiler option
- Updated help text to document new --compiler option
- Refactored compiler detection logic to prioritize --compiler over env vars
- Added target platform support checking for compilers
- Improved full-static build compiler resolution with better error handling
- Fixed musl library directory lookup to use bundled SDK location
- Added C++ runtime library linking for Linux/macOS builds
- Updated CLI argument parsing and help display accordingly
master
韩天峰 1 day ago
parent e07016b0bd
commit 4339ab56fc
  1. 11
      completions/tpc.bash
  2. 32
      phpunit/BashCompletionTest.php
  3. 61
      src/Backend/CompilerFactory.php
  4. 47
      src/Build/NativeBuildConfigurationTrait.php
  5. 2
      src/Build/SourcePipelineTrait.php
  6. 13
      src/Cli/BashCompletion.php
  7. 18
      src/Cli/CompletionMetadata.php
  8. 6
      src/Metadata/Constants.php
  9. 69
      src/Translator.php

@ -94,6 +94,10 @@ _typephp_tpc()
_typephp_tpc_complete_paths -f "$current"
return
;;
--compiler)
_typephp_tpc_complete_paths -c "$current"
return
;;
esac
case "$current" in
@ -134,8 +138,13 @@ _typephp_tpc()
done < <(compgen -d -- "$value")
return
;;
--compiler=*)
value="${current#--compiler=}"
COMPREPLY=( $(compgen -c -P '--compiler=' -- "$value") )
return
;;
-* )
COMPREPLY=( $(compgen -W '-O --optimize -o --output -h --help -v --version --profile --no-literal-strings --php-version -f --force -m --mode -r --run --debug -j --job --no-console --sanitize --cxx-std --march --target-platform --no-color --build-dir --dry -I --include-path -D --define --no-progress --lto --format -l --link-lib -L --link-path --full-static --wasm --wasm= --gen-python-helper --convert-python-to-php --output-dir --output-dir= --build-dir= --generate-completion=' -- "$current") )
COMPREPLY=( $(compgen -W '-O --optimize -o --output -h --help -v --version --profile --no-literal-strings --php-version -f --force -m --mode -r --run --debug -j --job --no-console --sanitize --cxx-std --march --compiler --target-platform --no-color --build-dir --dry -I --include-path -D --define --no-progress --lto --format -l --link-lib -L --link-path --full-static --wasm --wasm= --gen-python-helper --convert-python-to-php --output-dir --output-dir= --build-dir= --generate-completion=' -- "$current") )
return
;;
esac

@ -71,6 +71,38 @@ final class BashCompletionTest extends TestCase
);
}
/**
* --compiler must complete commands, not the default *.php/*.yml file set.
*
* Prefixes are chosen to exist on any POSIX box: "ec" matches the bash
* builtin `echo`, and /usr/bin/ always holds executables.
*/
public function testCompilerOptionCompletesCommandsAndExecutablePaths(): void
{
$script = TYPEPHP_ROOT_PATH . '/completions/tpc.bash';
$names = $this->complete($script, ['tpc', '--compiler', 'ec']);
self::assertNotSame('', $names);
self::assertStringContainsString('echo', $names);
foreach (explode("\n", trim($names)) as $candidate) {
self::assertDoesNotMatchRegularExpression('/\.(php|yml|yaml|prof)$/', $candidate);
}
// Once the word contains a slash, completion switches to that directory.
$paths = $this->complete($script, ['tpc', '--compiler', '/usr/bin/']);
self::assertNotSame('', $paths);
foreach (explode("\n", trim($paths)) as $candidate) {
self::assertStringStartsWith('/usr/bin/', $candidate);
}
// The --compiler= form keeps the prefix on every candidate.
$equals = $this->complete($script, ['tpc', '--compiler=ec']);
self::assertNotSame('', $equals);
foreach (explode("\n", trim($equals)) as $candidate) {
self::assertStringStartsWith('--compiler=', $candidate);
}
}
/** @param list<string> $words */
private function complete(string $script, array $words): string
{

@ -33,27 +33,11 @@ class CompilerFactory
}
/**
* Resolve the compiler command based on configuration, environment variables, and platform defaults.
* Resolve the compiler command from an explicit selection, falling back to the platform default.
*/
public static function detectCompilerName(PlatformBase $platform, string $configuredCompiler = ''): string
{
if ($configuredCompiler !== '') {
return $configuredCompiler;
}
$compilerEnv = getenv('PHPX_CC');
if ($compilerEnv) {
return $compilerEnv;
}
if (!$platform instanceof Windows) {
$cxxEnv = getenv('CXX');
if ($cxxEnv) {
return $cxxEnv;
}
}
return $platform->getDefaultCompiler();
return $configuredCompiler !== '' ? $configuredCompiler : $platform->getDefaultCompiler();
}
/**
@ -150,6 +134,47 @@ class CompilerFactory
return false;
}
/**
* Whether a compiler can actually build code for the given target triple.
*
* gcc rejects --target outright, and a wasm-only clang (wasi-sdk is often
* first on PATH in this project) accepts the flag but only fails once it
* has to create a target machine, so the probe must reach code generation.
*/
public static function supportsTarget(string $compilerName, string $target): bool
{
if (!self::isCommandExecutable($compilerName)) {
return false;
}
$token = @tempnam(sys_get_temp_dir(), 'tpc_target_probe');
if ($token === false) {
return false;
}
$source = $token . '.c';
$object = $token . '.o';
if (@file_put_contents($source, "int main(void) { return 0; }\n") === false) {
@unlink($token);
return false;
}
$nullDevice = DIRECTORY_SEPARATOR === '\\' ? 'NUL' : '/dev/null';
$command = escapeshellcmd($compilerName)
. ' ' . escapeshellarg('--target=' . $target)
. ' -c ' . escapeshellarg($source)
. ' -o ' . escapeshellarg($object)
. ' >' . $nullDevice . ' 2>&1';
$status = 0;
@exec($command, $output, $status);
@unlink($source);
@unlink($object);
@unlink($token);
return $status === 0;
}
public static function getCommandProgram(string $command): string
{
$command = trim($command);

@ -57,41 +57,20 @@ trait NativeBuildConfigurationTrait
* thread-local read (ZTS globals, and PHP's ZEND_TLS data) lands on the
* wrong address and the process crashes during php_module_startup.
*
* Resolution order: PHPX_MUSL_LIB_DIR, the bundled SDK, then the usual
* system locations.
* The startup files ship with the SDK, at phpx/full-static/sdk/lib/musl.
*/
protected function getFullStaticMuslDir(): string
{
$sdkDir = $this->getFullStaticSdkDir();
$arch = explode('-', $this->getFullStaticTargetTriple())[0];
$candidates = [];
$env = getenv('PHPX_MUSL_LIB_DIR');
if (is_string($env) && $env !== '') {
$candidates[] = $env;
}
if ($sdkDir !== null) {
$candidates[] = $sdkDir . '/lib/musl';
}
// Alpine and other musl-native systems keep the startup files in /usr/lib
$candidates[] = '/usr/lib/' . $arch . '-linux-musl';
$candidates[] = '/usr/lib/' . $arch . '-alpine-linux-musl';
$candidates[] = '/usr/lib/musl';
$candidates[] = '/usr/local/musl/lib';
$candidates[] = '/usr/lib';
foreach ($candidates as $dir) {
if (is_file($dir . '/crt1.o')) {
return $dir;
}
$muslDir = $sdkDir . '/lib/musl';
if (!is_file($muslDir . '/crt1.o')) {
$this->error(
"--full-static requires the musl startup files bundled with the SDK;\n"
. " crt1.o not found at: {$muslDir}\n"
. ' Rebuild the SDK with sapi/scripts/build-sdk.sh, which now copies them there.'
);
}
$this->error(
"--full-static requires musl startup files (crt1.o).\n"
. " Looked in: " . implode(', ', $candidates) . "\n"
. " Install musl (e.g. 'apt install musl-dev'), or copy crt1.o/crti.o/crtn.o from the\n"
. ' swoole-cli build container into ' . ($sdkDir ?? '<sdk>') . "/lib/musl, or set PHPX_MUSL_LIB_DIR."
);
return $muslDir;
}
protected function getIncludePaths(): array
@ -226,6 +205,14 @@ trait NativeBuildConfigurationTrait
$libraries[] = 'gmp';
$libraries[] = 'gmpxx';
$libraries[] = 'mpfr';
// The C++ runtime has to be requested explicitly: libphp.so carries
// none, and a C driver (clang) does not add it the way g++/clang++
// do. WASI needs nothing — its toolchain supplies the runtime.
if ($this->isLinux()) {
$libraries[] = 'stdc++';
} elseif ($this->isMacos()) {
$libraries[] = 'c++';
}
}
return $libraries;

@ -175,7 +175,7 @@ trait SourcePipelineTrait
$this->error(
"C/C++ compiler executable not found: {$program}\n" .
"Configured compiler command: {$compilerCommand}\n" .
"Install a supported compiler or set `cpp-compiler` in project.yml / PHPX_CC / CXX."
"Install a supported compiler, or select one with --compiler, or set `cpp-compiler` in project.yml."
);
}

@ -22,6 +22,7 @@ final class BashCompletion
$directoryOptions = implode('|', array_map(self::bashPattern(...), CompletionMetadata::directoryOptions()));
$pythonFileOptions = implode('|', array_map(self::bashPattern(...), CompletionMetadata::pythonFileOptions()));
$outputFileOptions = implode('|', array_map(self::bashPattern(...), CompletionMetadata::outputFileOptions()));
$commandOptions = implode('|', array_map(self::bashPattern(...), CompletionMetadata::commandOptions()));
$equalsCases = '';
foreach (CompletionMetadata::values() as $option => $values) {
if (!str_ends_with($option, '=')) {
@ -45,6 +46,14 @@ final class BashCompletion
. ' done < <(compgen -d -- "$value")' . "\n"
. " return\n ;;\n";
}
foreach (CompletionMetadata::commandEqualsOptions() as $option) {
$prefix = substr($option, 0, -1);
$equalsCases .= ' ' . self::bashPattern($prefix) . "=*)\n"
. ' value="${current#' . $prefix . '=}"' . "\n"
. ' COMPREPLY=( $(compgen -c -P ' . self::quote($prefix . '=')
. ' -- "$value") )' . "\n"
. " return\n ;;\n";
}
$script = <<<BASH
# Bash completion for the TypePHP compiler.
@ -115,6 +124,10 @@ _typephp_tpc()
_typephp_tpc_complete_paths -f "\$current"
return
;;
{$commandOptions})
_typephp_tpc_complete_paths -c "\$current"
return
;;
esac
case "\$current" in

@ -65,6 +65,24 @@ final class CompletionMetadata
return ['--convert-python-to-php'];
}
/**
* Options whose value is a command. `compgen -c` completes names found in
* PATH, and switches to executables in a directory once the word contains
* a slash, so both `clang++` and `/usr/bin/clang++` are covered.
*
* @return list<string>
*/
public static function commandOptions(): array
{
return ['--compiler'];
}
/** @return list<string> */
public static function commandEqualsOptions(): array
{
return ['--compiler='];
}
/** @return list<string> */
public static function outputFileOptions(): array
{

@ -232,6 +232,12 @@ class Constants
'required' => false,
'defaultValue' => '',
],
'compiler' => [
'longPrefix' => 'compiler',
'description' => 'C++ compiler command to use (e.g. --compiler=/usr/bin/clang)',
'required' => false,
'defaultValue' => '',
],
'target-platform' => [
'longPrefix' => 'target-platform',
'description' => 'Cross-compilation target triple (e.g. aarch64-linux-gnu, x86_64-w64-mingw32)',

@ -214,20 +214,21 @@ class Translator extends Preprocessor
} else {
$this->platform = PlatformFactory::create();
}
// --compiler wins over the platform default; WASI keeps its own
// default because that platform also drives --sysroot and friends.
$this->cppCompiler = $this->platform instanceof Wasi
? $this->platform->getDefaultCompiler()
: CompilerFactory::detectCompilerName($this->platform);
: CompilerFactory::detectCompilerName($this->platform, $this->getCommandLineCompiler());
// --full-static: libphp.a embeds musl libc, so the link must produce a
// musl binary. That is driven by clang's --target=<arch>-unknown-linux-musl
// plus -static -B <musl dir>, which gcc does not accept; a glibc link
// would let musl's __libc_start_main install a thread pointer whose
// layout does not match the linker's TLS offsets, corrupting every
// thread-local read in PHP's ZTS globals. PHPX_CC/CXX take precedence,
// otherwise fall back to clang.
// thread-local read in PHP's ZTS globals.
if ($this->climate->arguments->defined('full-static')) {
$this->fullStatic = true;
$this->cppCompiler = getenv('PHPX_CC') ?: (getenv('CXX') ?: 'clang');
$this->cppCompiler = $this->detectFullStaticCompiler();
}
if ($this->platform instanceof Windows) {
@ -259,6 +260,54 @@ class Translator extends Preprocessor
}
}
/**
* The compiler requested with --compiler, or '' when the flag was not given.
*/
protected function getCommandLineCompiler(): string
{
if (!$this->climate->arguments->defined('compiler')) {
return '';
}
return trim((string) $this->climate->arguments->get('compiler'));
}
/**
* Resolve a compiler able to build the fully-static musl target.
*
* Only a native clang can drive --target=<arch>-unknown-linux-musl, so the
* platform default (g++ on Linux) is never usable. --compiler is honoured
* but never silently replaced; otherwise PATH's first clang is tried and a
* standard location is used as a fallback — a wasm-only clang such as
* wasi-sdk sits first on PATH in this project and would otherwise fail
* halfway through the build with an opaque "no available targets" error.
*/
protected function detectFullStaticCompiler(): string
{
$target = $this->getFullStaticTargetTriple();
$explicit = $this->getCommandLineCompiler();
if ($explicit !== '') {
if (!CompilerFactory::supportsTarget($explicit, $target)) {
$this->error(
"--full-static requires a clang that can target {$target}, but {$explicit} cannot.\n"
. " Pass a native clang, for example: --compiler=/usr/bin/clang"
);
}
return $explicit;
}
foreach (['clang', '/usr/bin/clang'] as $candidate) {
if (CompilerFactory::supportsTarget($candidate, $target)) {
return $candidate;
}
}
$this->error(
"--full-static requires a clang that can target {$target}; none was found.\n"
. " Install clang, or pass it explicitly, for example: --compiler=/usr/bin/clang"
);
}
public function parseArgv(array $argv)
{
$path = null;
@ -312,6 +361,7 @@ class Translator extends Preprocessor
$climate->tab()->out('-r, --run Run the compiled binary after build');
$climate->tab()->out('-j, --job <num> Number of parallel compilation jobs (default: 4)');
$climate->tab()->out('--cxx-std <ver> C++ standard version (c++17, c++20, etc., default: c++17)');
$climate->tab()->out('--compiler <cmd> C++ compiler command to use (e.g. --compiler=/usr/bin/clang)');
$climate->tab()->out('--march <arch> Target CPU instruction set (e.g. native, x86-64-v3, armv8-a)');
$climate->tab()->out('--target-platform <triple> Cross-compilation target triple (e.g. aarch64-linux-gnu)');
$climate->tab()->out('--wasm[=profile] Build WASI component (default) or browser output');
@ -332,6 +382,7 @@ class Translator extends Preprocessor
$climate->tab()->out('--format Enable clang-format code formatting (disabled by default)');
$climate->tab()->out('-l, --link-lib <lib> Link against a library (repeatable, e.g. -lcurl)');
$climate->tab()->out('-L, --link-path <dir> Add a library search path (repeatable, e.g. -L/usr/local/lib)');
$climate->tab()->out('--full-static Link fully statically against the bundled SDK (phpx/full-static/sdk)');
$climate->br();
}
@ -353,6 +404,16 @@ class Translator extends Preprocessor
$this->setBuildMode($this->climate->arguments->get('mode'));
}
// --full-static links the whole musl C runtime into the artifact, so it
// only makes sense for a self-contained executable. A shared object
// would drag a second, incompatible libc into the host process.
if ($this->fullStatic && !$this->isBuildModeBin()) {
$this->error(
'--full-static is only supported in bin mode (-m bin); '
. 'shared libraries and extensions cannot embed a C runtime.'
);
}
// Debug line number
if ($this->climate->arguments->defined('debug-line')) {
$this->debugLine = intval($this->climate->arguments->get('debug-line'));

Loading…
Cancel
Save