-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtphp.php
More file actions
1662 lines (1567 loc) · 77.2 KB
/
Copy pathtphp.php
File metadata and controls
1662 lines (1567 loc) · 77.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env php
<?php
declare(strict_types=1);
// ============================================================
// TinyPHP — PHP → C transpiler (multi-file support)
//
// Usage:
// tphp <file.php> [<file2.php> ...] [-o <output.exe>]
// tphp . compile all .php in current dir
// tphp -f <file.php> [-o <output.exe>]
// ============================================================
/** TinyPHP 版本号 */
const TPHP_VERSION = '0.2.0-beta.8';
spl_autoload_register(function (string $class): void {
$baseDir = __DIR__ . '/src';
$parts = explode('\\', $class);
$file = $baseDir . '/' . implode('/', $parts) . '.php';
if (file_exists($file)) require_once $file;
});
require_once __DIR__ . '/src/TokenType.php';
require_once __DIR__ . '/src/Token.php';
require_once __DIR__ . '/src/AST/Node.php';
require_once __DIR__ . '/src/Lexer.php';
require_once __DIR__ . '/src/Parser.php';
require_once __DIR__ . '/src/CodeGenerator.php';
require_once __DIR__ . '/src/Compiler.php';
// SSA 路径相关模块(仅 --ssa 模式使用,但 require_once 开销可忽略)
require_once __DIR__ . '/src/AST/FlatAst.php';
require_once __DIR__ . '/src/AST/FlatAstConverter.php';
require_once __DIR__ . '/src/SSA/SSA.php';
require_once __DIR__ . '/src/SSA/SSABuilder.php';
require_once __DIR__ . '/src/SSA/SSAOptPass.php';
require_once __DIR__ . '/src/SSA/SSAToCGenerator.php';
// --- Parse arguments ---
$options = getopt('f:o:hv', ['help', 'os:', 'arch:', 'debug', 'version']);
$cc = null;
$targetOS = null; // -os windows|linux|macos
$targetArch = null; // -arch x86_64|aarch64
$isShared = false; // -shared: 生成动态库
// Normalize arch name
$archMap = ['x86_64' => 'x86_64', 'amd64' => 'x86_64', 'x64' => 'x86_64',
'aarch64' => 'aarch64', 'arm64' => 'aarch64', 'arm' => 'arm'];
// Manual parse -cc xxx, -os xxx, -arch xxx and -o xxx (PHP getopt not fully compatible)
$posArgs = [];
for ($i = 1, $n = count($argv); $i < $n; $i++) {
if ($argv[$i] === '-cc' && isset($argv[$i + 1])) {
$cc = $argv[++$i];
} elseif ($argv[$i] === '-arch' && isset($argv[$i + 1])) {
$targetArch = $archMap[strtolower($argv[++$i])] ?? null;
if ($targetArch === null) die("Error: unknown arch '{$argv[$i-1]}'. Use: x86_64, aarch64\n");
} elseif ($argv[$i] === '-os' && isset($argv[$i + 1])) {
$targetOS = strtolower($argv[++$i]);
// Normalize: macos → darwin
if ($targetOS === 'macos' || $targetOS === 'mac') $targetOS = 'darwin';
} elseif ($argv[$i] === '-o' && isset($argv[$i + 1])) {
$outExe = $argv[++$i]; // 覆盖 getopt 解析
} elseif ($argv[$i] === '-shared') {
$isShared = true;
} elseif (!str_starts_with($argv[$i], '-')) {
$posArgs[] = $argv[$i];
}
}
$args = $posArgs;
// Also check --os=xxx, --arch=xxx long form
if (isset($options['os'])) {
$targetOS = strtolower($options['os']);
if ($targetOS === 'macos' || $targetOS === 'mac') $targetOS = 'darwin';
}
if (isset($options['arch']) && $targetArch === null) {
$targetArch = $archMap[strtolower($options['arch'])] ?? null;
if ($targetArch === null) die("Error: unknown arch '{$options['arch']}'. Use: x86_64, aarch64\n");
}
// Default arch per target OS: Windows/Linux → x86_64, macOS → aarch64
if ($targetOS !== null && $targetArch === null) {
$targetArch = ($targetOS === 'darwin') ? 'aarch64' : 'x86_64';
}
if (isset($options['f'])) {
$args = array_merge([$options['f']], array_diff($args, [$options['f']]));
}
if (isset($options['version']) || isset($options['v'])) {
echo 'TinyPHP ' . TPHP_VERSION . "\n";
exit(0);
}
if ((empty($args) && !isset($options['f'])) || isset($options['h']) || isset($options['help'])) {
showHelp();
}
$outExe = $outExe ?? $options['o'] ?? '';
// Convert relative output path to absolute — TCC may chdir() to its binary dir,
// so a relative -o path would land in the wrong place.
if ($outExe !== '' && !str_starts_with($outExe, '/') && !preg_match('#^[A-Za-z]:#', $outExe)) {
$outExe = getcwd() . DIRECTORY_SEPARATOR . $outExe;
}
// --- Collect all source files ---
[$files, $userCFiles] = collectFiles($args);
if (empty($files)) {
die("Error: no .php files found\n");
}
// First file used for naming
$entryFile = $files[0];
// Paths
$cwd = getcwd();
$includeDir = __DIR__ . DIRECTORY_SEPARATOR . 'include';
// PHAR mode: extract include/ and tcc/ alongside the PHAR (TCC can't read phar://)
$inPhar = str_starts_with(__DIR__, 'phar://');
$pharDir = '';
if ($inPhar) {
$pharDir = dirname(Phar::running(false));
// Extract TinyPHP headers (first run only)
$pharIncludeDir = $includeDir;
$destIncludeDir = $pharDir . DIRECTORY_SEPARATOR . 'include';
if (!is_dir($destIncludeDir)) {
extractPharDir($pharIncludeDir, $destIncludeDir);
}
// Extract TCC compiler (first run only)
$pharRoot = dirname($includeDir);
$pharTccDir = $pharRoot . '/tcc';
$destTccDir = $pharDir . DIRECTORY_SEPARATOR . 'tcc';
if (!is_dir($destTccDir) && is_dir($pharTccDir)) {
extractPharDir($pharTccDir, $destTccDir);
}
// Extract ext/ (first run only)
$pharExtDir = $pharRoot . '/ext';
$destExtDir = $pharDir . DIRECTORY_SEPARATOR . 'ext';
if (!is_dir($destExtDir) && is_dir($pharExtDir)) {
extractPharDir($pharExtDir, $destExtDir);
}
$includeDir = $destIncludeDir;
$extRootPhar = $destExtDir; // #import 使用解压后的 ext/
}
// Compiler selection: -cc for external compiler, otherwise built-in TCC
if ($cc !== null) {
$ccExe = $cc;
// If it's a bare name (no path separator), rely on system PATH
if (!str_contains($ccExe, '/') && !str_contains($ccExe, '\\')) {
// Don't check file existence, let exec handle it
} elseif (!file_exists($ccExe)) {
die("Error: specified compiler not found: {$ccExe}\n");
}
} elseif ($inPhar) {
// PHAR mode: use built-in TCC extracted alongside the PHAR
$tccBase = $pharDir . DIRECTORY_SEPARATOR . 'tcc';
if (PHP_OS_FAMILY === 'Windows') {
$ccExe = $tccBase . DIRECTORY_SEPARATOR . 'win32' . DIRECTORY_SEPARATOR . 'tcc.exe';
} else {
$ccExe = $tccBase . DIRECTORY_SEPARATOR . 'tcc';
if (file_exists($ccExe)) chmod($ccExe, 0755);
}
if (!file_exists($ccExe)) die("Error: built-in TCC not found in PHAR: {$ccExe}\nMake sure tcc/ exists when building the PHAR\n");
} else {
// Dev mode: TCC is alongside the project
$ccExe = __DIR__ . DIRECTORY_SEPARATOR . 'tcc'
. (PHP_OS_FAMILY === 'Windows'
? DIRECTORY_SEPARATOR . 'win32' . DIRECTORY_SEPARATOR . 'tcc.exe'
: DIRECTORY_SEPARATOR . 'tcc');
if (!file_exists($ccExe)) die("Error: built-in TCC not found: {$ccExe}\nBuild TCC first or use -cc to specify another compiler\n");
}
if (!is_dir($includeDir)) die("Error: include directory not found: {$includeDir}\n");
// Detect compiler class early — used by Parser for #if 条件编译
// TCC (built-in), GCC, or Clang
$ccClass = 'TCC';
if ($cc !== null) {
$ccLower = strtolower($cc);
if (str_contains($ccLower, 'gcc')) $ccClass = 'GCC';
elseif (str_contains($ccLower, 'clang')) $ccClass = 'Clang';
} elseif (PHP_OS_FAMILY === 'Darwin') {
$ccClass = 'Clang';
}
// 目标 OS/Arch(条件编译求值用):未指定时回退到宿主环境
$ctTargetOS = $targetOS ?? strtolower(PHP_OS_FAMILY);
$ctTargetArch = $targetArch ?? strtolower(php_uname('m'));
// --- Phase 1: Transpile all PHP → C ---
$allFilesStr = implode(', ', array_map(fn($f) => basename($f), $files));
echo "[1/2] Transpiling {$allFilesStr} => C...\n";
try {
$mainClass = null;
$extraClasses = [];
$functions = [];
$constants = [];
$enums = [];
$allIncludes = [];
$allFlags = [];
$allCallbacks = [];
$allDebugs = [];
$allCstructs = [];
// Two-phase parsing: parse auxiliary files (non-Main) first,
// collect enums/classes, then parse Main entry last.
// Ensures cross-file enums are known when parsing Main.
$mainFile = null;
$otherFiles = [];
// ── #import 预扫描:引入 ext/name/src/*.php → $files ────
// 用 for 而非 foreach:扩展文件可能有自己的 #import,需递归扫描
$extRoot = $inPhar ? ($extRootPhar ?? __DIR__ . DIRECTORY_SEPARATOR . 'ext') : (__DIR__ . DIRECTORY_SEPARATOR . 'ext');
$importedExts = []; // 已处理的扩展名,避免重复
// Magic constants for #include / #flag
// PHAR 模式:__EXT__ 必须指向文件系统解压路径,否则 #include 无法解析
$magicExt = $inPhar
? str_replace('\\', '/', $destExtDir)
: str_replace('\\', '/', realpath(__DIR__ . '/ext') ?: __DIR__ . '/ext');
$magicInc = str_replace('\\', '/', realpath($includeDir) ?: $includeDir);
$magicCmd = str_replace('\\', '/', $cwd);
for ($fi = 0; $fi < count($files); $fi++) {
$src = file_get_contents($files[$fi]);
// Preprocess: expand magic constants in #include directives
$filePath = realpath($files[$fi]);
$fileDir = dirname($filePath);
$src = preg_replace_callback(
'/^(#include\s+)(?:(Windows|Linux|MacOS|Darwin|GCC|Clang|TCC)\s+)?(.+)$/mi',
function ($m) use ($fileDir, $magicExt, $magicInc, $magicCmd) {
$prefix = $m[2] ?? '';
$inc = $m[3];
$prefixPart = $prefix !== '' ? $m[2] . ' ' : '';
// Already quoted or system header → leave as-is
if (str_starts_with($inc, '"') || str_starts_with($inc, '<')) {
return $m[0];
}
// Expand magic constants
$inc = str_replace('__DIR__', $fileDir, $inc);
$inc = str_replace('__EXT__', $magicExt, $inc);
$inc = str_replace('__INC__', $magicInc, $inc);
$inc = str_replace('__CMD__', $magicCmd, $inc);
$inc = str_replace('DIRECTORY_SEPARATOR', DIRECTORY_SEPARATOR, $inc);
$inc = str_replace('\\', '/', $inc); // normalize Windows backslashes for PCRE
$inc = rtrim($inc, "\r\n"); // strip trailing CR from .+ match on Windows
// Wrap in quotes (simplify: strip . concatenation noise)
$inc = preg_replace('/\s*\.\s*"/', '/', $inc);
$inc = preg_replace('/"\s*\.\s*/', '', $inc);
$inc = trim($inc, '" ');
return $m[1] . $prefixPart . '"' . $inc . '"';
},
(string)$src
);
// Preprocess: expand magic constants in #flag directives
$src = preg_replace_callback(
'/^(#flag\s+(?:GCC|Clang|TCC|Windows|Linux|MacOS|Darwin)?\s*(?:GCC|Clang|TCC|Windows|Linux|MacOS|Darwin)?\s*)(.+)$/mi',
function ($m) use ($fileDir, $magicExt, $magicInc, $magicCmd) {
$prefix = $m[1];
$flags = $m[2];
// Expand magic constants
$flags = str_replace('__DIR__', str_replace('\\', '/', $fileDir), $flags);
$flags = str_replace('__EXT__', $magicExt, $flags);
$flags = str_replace('__INC__', $magicInc, $flags);
$flags = str_replace('__CMD__', $magicCmd, $flags);
// Handle string concatenation: -I__DIR__ . "include" → -I__DIR__/include
// Replace . " with / (insert path separator, not empty string)
$flags = preg_replace('/\s*\.\s*"/', '/', $flags);
$flags = preg_replace('/"\s*\.\s*/', '/', $flags);
$flags = str_replace('"', '', $flags); // remove remaining quotes
$flags = str_replace('\\', '/', $flags);
return $prefix . $flags;
},
(string)$src
);
if (preg_match_all('/^#import\s+(\w+)/m', (string)$src, $m)) {
foreach ($m[1] as $extName) {
if (isset($importedExts[$extName])) continue; // 已导入,跳过
// Security: #import only accepts plain extension names (no paths)
if (str_contains($extName, '..') || str_contains($extName, '/') || str_contains($extName, '\\')) {
die("Error: #import '{$extName}' contains path traversal — only extension names are allowed\n");
}
$importedExts[$extName] = true;
$extSrc = $extRoot . DIRECTORY_SEPARATOR . $extName . DIRECTORY_SEPARATOR . 'src';
// Security: resolve via realpath and verify the path stays within ext/
$extSrcReal = realpath($extSrc);
if ($extSrcReal === false || !str_starts_with($extSrcReal, realpath($extRoot))) {
die("Error: #import '{$extName}' resolves outside the extensions directory\n");
}
$extSrc = $extSrcReal;
if (!is_dir($extSrc)) die("Error: #import {$extName} — ext/{$extName}/src/ not found\n");
// #import 只收集 .php 文件;C 依赖由 ext 的 .php 通过 #flag 显式声明
// (如 #flag __EXT__ . "name/src/name.c"),符合 phpc 显式模型
$extPhp = glob($extSrc . DIRECTORY_SEPARATOR . '*.php');
foreach ($extPhp as $f) { if (!in_array($f, $files)) $files[] = $f; }
echo " #import {$extName} => " . count($extPhp) . " php\n";
}
}
}
foreach ($files as $file) {
// Quick check: does file contain class Main (global namespace)?
$src = file_get_contents($file);
if (preg_match('/^\s*class\s+Main\b/m', (string)$src)) {
$mainFile = $file;
} else {
$otherFiles[] = $file;
}
}
if ($mainFile === null) {
die("Error: no global class Main found (entry class must be named Main in the global namespace)\n");
}
$entryFile = $mainFile;
// --debug: enable #debug directive and print compile command
// (manual parse, because getopt stops at first positional argument)
$debugMode = in_array('--debug', $argv, true);
// --ssa: 启用 SSA 中间表示路径(FlatAst → SSA → 优化 → C)
$ssaMode = in_array('--ssa', $argv, true);
// Collect known enum names (for cross-file references)
$knownEnumNames = [];
$orderedFiles = array_merge($otherFiles, [$mainFile]);
foreach ($orderedFiles as $file) {
echo " + {$file}\n";
$source = file_get_contents($file);
if ($source === false || trim($source) === '') {
die("Error: PHP file is empty: {$file}\n");
}
// Preprocess: expand magic constants in #include directives
$fileDir = dirname(realpath($file));
$source = preg_replace_callback(
'/^(#include\s+)(?:(Windows|Linux|MacOS|Darwin|GCC|Clang|TCC)\s+)?(.+)$/mi',
function ($m) use ($fileDir, $magicExt, $magicInc, $magicCmd) {
$prefix = $m[2] ?? '';
$inc = $m[3];
$prefixPart = $prefix !== '' ? $m[2] . ' ' : '';
if (str_starts_with($inc, '"') || str_starts_with($inc, '<')) {
return $m[0];
}
$inc = str_replace('__DIR__', $fileDir, $inc);
$inc = str_replace('__EXT__', $magicExt, $inc);
$inc = str_replace('__INC__', $magicInc, $inc);
$inc = str_replace('__CMD__', $magicCmd, $inc);
$inc = str_replace('DIRECTORY_SEPARATOR', DIRECTORY_SEPARATOR, $inc);
$inc = str_replace('\\', '/', $inc); // normalize Windows backslashes for PCRE
$inc = rtrim($inc, "\r\n"); // strip trailing CR from .+ match on Windows
$inc = preg_replace('/\s*\.\s*"/', '/', $inc);
$inc = preg_replace('/"\s*\.\s*/', '', $inc);
$inc = trim($inc, '" ');
return $m[1] . $prefixPart . '"' . $inc . '"';
},
(string)$source
);
// Preprocess: expand magic constants in #flag directives
$source = preg_replace_callback(
'/^(#flag\s+(?:GCC|Clang|TCC|Windows|Linux|MacOS|Darwin)?\s*(?:GCC|Clang|TCC|Windows|Linux|MacOS|Darwin)?\s*)(.+)$/mi',
function ($m) use ($fileDir, $magicExt, $magicInc, $magicCmd) {
$prefix = $m[1];
$flags = $m[2];
$flags = str_replace('__DIR__', str_replace('\\', '/', $fileDir), $flags);
$flags = str_replace('__EXT__', $magicExt, $flags);
$flags = str_replace('__INC__', $magicInc, $flags);
$flags = str_replace('__CMD__', $magicCmd, $flags);
// Handle string concatenation: -I__DIR__ . "include" → -I__DIR__/include
// Replace . " with / (insert path separator, not empty string)
$flags = preg_replace('/\s*\.\s*"/', '/', $flags);
$flags = preg_replace('/"\s*\.\s*/', '/', $flags);
$flags = str_replace('"', '', $flags);
$flags = str_replace('\\', '/', $flags);
return $prefix . $flags;
},
(string)$source
);
$lexer = new Lexer($source, $debugMode);
$tokens = $lexer->tokenize();
$parser = new Parser($tokens, $debugMode, $ctTargetOS, $ctTargetArch, $ccClass);
// Inject enum names declared in other files (for cross-file enum references)
$parser->setKnownEnums($knownEnumNames);
$ast = $parser->parse();
// Merge AST — find global class Main from main + auxiliary classes
$candidates = array_merge(
$ast->mainClass ? [$ast->mainClass] : [],
$ast->extraClasses
);
foreach ($candidates as $cls) {
if ($cls->name === 'Main' && $cls->namespace === '') {
if ($mainClass !== null) {
die("Error: multiple global class Main declarations found\n");
}
$mainClass = $cls;
} else {
$extraClasses[] = $cls;
}
}
$functions = array_merge($functions, $ast->functions);
$constants = array_merge($constants, $ast->constants);
$enums = array_merge($enums, $ast->enums);
$allIncludes = array_merge($allIncludes, $ast->includes);
$allFlags = array_merge($allFlags, $ast->ccFlags);
$allCallbacks = array_merge($allCallbacks, $ast->callbacks);
$allDebugs = array_merge($allDebugs, $ast->debugs);
$allCstructs = array_merge($allCstructs, $ast->cstructs);
// Collect enum names (FQN) declared in this file for later files
foreach ($ast->enums as $e) {
$fq = ($e->namespace !== '')
? $e->namespace . '\\' . $e->name
: $e->name;
$knownEnumNames[$fq] = true;
}
}
if ($mainClass === null) {
die("Error: no global class Main found (entry class must be named Main in the global namespace)\n");
}
// Output path (derived from entry filename, respect -os target)
if ($outExe === '') {
$ext = ($targetOS === null)
? ((PHP_OS_FAMILY === 'Windows') ? '.exe' : '')
: (($targetOS === 'windows') ? '.exe' : '');
$outExe = $cwd . DIRECTORY_SEPARATOR . pathinfo($entryFile, PATHINFO_FILENAME) . $ext;
}
$outDir = $cwd . DIRECTORY_SEPARATOR . 'build';
// Clean build directory before compiling
// 只清理 build/ 下的直接文件(.c/.o/.exe 等),保留子目录(如 build/bench/)
// rmdir 可能因子目录存在而失败,用 @ 抑制 warning
if (is_dir($outDir)) {
$contents = glob($outDir . DIRECTORY_SEPARATOR . '*');
if ($contents !== false) {
foreach ($contents as $f) { if (is_file($f)) unlink($f); }
}
@rmdir($outDir);
}
// Dedup: #include by file, #flag by flags string
$seenFiles = [];
$allIncludes = array_values(array_filter($allIncludes, function ($inc) use (&$seenFiles) {
$f = is_array($inc) ? $inc['file'] : $inc;
if (isset($seenFiles[$f])) return false;
$seenFiles[$f] = true;
// Platform/compiler filtering (#include Linux "x.h" / #include Windows "y.h")
if (is_array($inc) && !empty($inc['ctx'])) {
$ctx = $inc['ctx'];
// Case-insensitive platform matching (accept windows/linux/macos/darwin lowercase)
$platformMap = ['windows' => 'Windows', 'linux' => 'Linux', 'darwin' => 'Darwin', 'macos' => 'Darwin'];
$ctxLower = strtolower($ctx);
$currentOS = PHP_OS_FAMILY;
// OS filter
if (isset($platformMap[$ctxLower]) && $platformMap[$ctxLower] !== $currentOS) return false;
// Compiler filter (TCC/GCC/Clang)
if (!isset($platformMap[$ctxLower])) {
$ccLower = strtolower($GLOBALS['cc'] ?? 'tcc');
$ccClass = 'TCC';
if (str_contains($ccLower, 'gcc')) $ccClass = 'GCC';
elseif (str_contains($ccLower, 'clang')) $ccClass = 'Clang';
if ($ctx !== $ccClass) return false;
}
}
return true;
}));
$seenFlags = [];
$allFlags = array_values(array_filter($allFlags, function ($f) use (&$seenFlags) {
$s = $f['flags'] ?? '';
if (isset($seenFlags[$s])) return false;
$seenFlags[$s] = true;
return true;
}));
$merged = new ProgramNode($mainClass, $extraClasses, $functions, $constants, $enums, $allIncludes, $allFlags, $allCallbacks, $allDebugs, $allCstructs);
// Resolve #include paths relative to each PHP file's directory
$extraFlags = '';
$extraCFiles = [];
if (!empty($allIncludes)) {
// Collect unique directories from all PHP source files
$srcDirs = [];
foreach ($orderedFiles as $f) {
$d = realpath(dirname($f));
if ($d) $srcDirs[$d] = true;
}
$srcDirs = array_keys($srcDirs);
$extraFlags = ' -I"' . implode('" -I"', $srcDirs) . '"';
// Extract -I paths from #flag directives (for #include search + security check)
// __DIR__/__EXT__/__INC__/__CMD__ already expanded in prescan/parsing phase
$flagIncludeDirs = [];
$_platformMap = ['Windows' => 'Windows', 'Linux' => 'Linux', 'Darwin' => 'Darwin', 'MacOS' => 'Darwin'];
$_currentOS = PHP_OS_FAMILY;
$_ccClass = 'TCC';
if ($cc !== null) {
$_ccLower = strtolower($cc);
if (str_contains($_ccLower, 'gcc')) $_ccClass = 'GCC';
elseif (str_contains($_ccLower, 'clang')) $_ccClass = 'Clang';
} elseif (PHP_OS_FAMILY === 'Darwin') {
$_ccClass = 'Clang';
}
foreach ($allFlags as $f) {
$pf = $f['platform'] ?? '';
$cf = $f['compiler'] ?? '';
$flagsStr = $f['flags'] ?? '';
$platformOk = ($pf === '' || ($_platformMap[$pf] ?? '') === $_currentOS);
$compilerOk = ($cf === '' || $cf === $_ccClass);
if (!$platformOk || !$compilerOk) continue;
// Extract -I paths (flagsStr already has __DIR__ expanded)
$_tokens = preg_split('/\s+/', trim($flagsStr));
foreach ($_tokens as $tok) {
if (str_starts_with($tok, '-I') && strlen($tok) > 2) {
$path = substr($tok, 2);
// Strip surrounding quotes
$path = trim($path, '"');
$resolved = realpath($path);
if ($resolved !== false) {
$flagIncludeDirs[$resolved] = true;
}
}
}
}
$flagIncludeDirs = array_keys($flagIncludeDirs);
// All search directories: srcDirs + -I paths from #flag
$allSearchDirs = array_merge($srcDirs, $flagIncludeDirs);
// Find companion .c files for each #include
$projectRoot = str_replace('\\', '/', __DIR__);
// PHAR 模式:解压到文件系统的 ext/ 不在 phar:// 路径下,需额外接受 PHAR 外部根
$fsProjectRoot = $inPhar ? str_replace('\\', '/', $pharDir) : $projectRoot;
// Allowed roots for security check:
// - TinyPHP project root (built-in includes)
// - PHAR fs root
// - User source directories (where PHP files are)
// - -I paths declared via #flag (user explicitly opted in)
// - CWD (user's project root)
$allowedRoots = [$projectRoot, $fsProjectRoot];
foreach ($allSearchDirs as $dir) {
$allowedRoots[] = str_replace('\\', '/', $dir);
}
$allowedRoots[] = str_replace('\\', '/', realpath($cwd) ?: $cwd);
foreach ($allIncludes as $inc) {
$fileName = is_array($inc) ? $inc['file'] : $inc;
$isQuoted = is_array($inc) ? ($inc['quoted'] ?? true) : true;
// System headers (#include <math.h>) — 白名单校验
if (!$isQuoted) {
// 安全加固: 系统头文件白名单(防止任意引入系统 API)
// 允许标准 C 库头文件 + 常见系统头
$allowedSystemHeaders = [
// C 标准库
'stdio.h','stdlib.h','string.h','math.h','ctype.h','time.h',
'stdint.h','stddef.h','stdbool.h','stdarg.h','limits.h','float.h',
'errno.h','assert.h','locale.h','setjmp.h','signal.h','wchar.h',
'wctype.h','iso646.h','fenv.h','inttypes.h','complex.h','tgmath.h',
'iconv.h',
// POSIX 常用
'unistd.h','fcntl.h','sys/stat.h','sys/types.h','sys/wait.h',
'sys/time.h','sys/socket.h','sys/un.h','sys/mman.h','sys/resource.h',
'netinet/in.h','netinet/tcp.h','arpa/inet.h','netdb.h','pthread.h',
'dlfcn.h','poll.h','select.h','termios.h','pty.h','semaphore.h',
'dirent.h','utime.h','sys/utsname.h','sys/file.h','sys/ioctl.h',
// Windows 常用
'windows.h','winsock2.h','ws2tcpip.h','io.h','process.h','direct.h',
'conio.h','shlobj.h','shellapi.h','wincrypt.h','winreg.h',
// C++ 兼容
'cstring','cstdlib','cstdio','cmath','cstdint','vector','string','map',
// mbedtls(本地源码编译,由 ext/openssl 扩展使用,通过 -I 路径查找)
'mbedtls/aes.h','mbedtls/aria.h','mbedtls/asn1.h','mbedtls/asn1write.h',
'mbedtls/base64.h','mbedtls/bignum.h','mbedtls/block_cipher.h',
'mbedtls/build_info.h','mbedtls/camellia.h','mbedtls/ccm.h','mbedtls/chacha20.h',
'mbedtls/chachapoly.h','mbedtls/check_config.h','mbedtls/cipher.h','mbedtls/cmac.h',
'mbedtls/compat-2.x.h','mbedtls/constant_time.h','mbedtls/ctr_drbg.h',
'mbedtls/debug.h','mbedtls/des.h','mbedtls/dhm.h','mbedtls/ecdh.h',
'mbedtls/ecdsa.h','mbedtls/ecjpake.h','mbedtls/ecp.h','mbedtls/entropy.h',
'mbedtls/error.h','mbedtls/gcm.h','mbedtls/hkdf.h','mbedtls/hmac_drbg.h',
'mbedtls/lms.h','mbedtls/md.h','mbedtls/md5.h','mbedtls/memory_buffer_alloc.h',
'mbedtls/net_sockets.h','mbedtls/nist_kw.h','mbedtls/oid.h','mbedtls/pem.h',
'mbedtls/pk.h','mbedtls/pkcs12.h','mbedtls/pkcs5.h','mbedtls/pkcs7.h',
'mbedtls/platform.h','mbedtls/platform_time.h','mbedtls/platform_util.h',
'mbedtls/poly1305.h','mbedtls/private_access.h','mbedtls/psa_util.h',
'mbedtls/ripemd160.h','mbedtls/rsa.h','mbedtls/sha1.h','mbedtls/sha256.h',
'mbedtls/sha3.h','mbedtls/sha512.h','mbedtls/ssl.h','mbedtls/ssl_cache.h',
'mbedtls/ssl_ciphersuites.h','mbedtls/ssl_cookie.h','mbedtls/ssl_ticket.h',
'mbedtls/threading.h','mbedtls/timing.h','mbedtls/version.h','mbedtls/x509.h',
'mbedtls/x509_crl.h','mbedtls/x509_crt.h','mbedtls/x509_csr.h',
];
$cleanName = ltrim($fileName, '/');
if (!in_array($cleanName, $allowedSystemHeaders, true)) {
// 允许 sys/ 和 net/ 和 arpa/ 和 netinet/ 前缀的系统头
$isAllowedPrefix = preg_match('/^(sys|net|arpa|netinet|netpacket|protocols)\//', $cleanName);
if (!$isAllowedPrefix) {
die("Error: #include <{$fileName}> is not in the system header whitelist.\n"
. " Allowed: standard C library headers, common POSIX/Windows headers.\n"
. " If you need this header, add it to the whitelist in tphp.php.\n");
}
}
continue;
}
// Security: resolve via realpath, verify within allowed roots
$resolvedInclude = null;
// Helper: check if a candidate path is within any allowed root
$isAllowed = function (string $candidate) use ($allowedRoots): bool {
foreach ($allowedRoots as $root) {
if (str_starts_with($candidate, $root)) return true;
}
return false;
};
// Absolute path (from __INC__/__EXT__/__CMD__ expansion): resolve directly
if (str_starts_with($fileName, '/') || preg_match('/^[A-Za-z]:/', $fileName)) {
$raw = realpath($fileName);
if ($raw !== false) {
$candidate = str_replace('\\', '/', $raw);
if ($isAllowed($candidate)) {
$resolvedInclude = $candidate;
}
}
} else {
// Relative path: resolve against source dirs + -I paths from #flag
foreach ($allSearchDirs as $dir) {
$raw = realpath($dir . DIRECTORY_SEPARATOR . $fileName);
if ($raw === false) continue;
$candidate = str_replace('\\', '/', $raw);
if ($isAllowed($candidate)) {
$resolvedInclude = $candidate;
break;
}
}
}
if ($resolvedInclude === null) {
die("Error: #include '{$fileName}' resolves outside the project or does not exist\n"
. " Project root: {$projectRoot}\n"
. " Search dirs: " . implode(', ', $allSearchDirs) . "\n"
. " Hint: use #flag -I__DIR__. \"your/include/path\" to add include search paths\n");
}
// #include 只负责引入头文件;同名 .c 依赖由 #flag 显式声明
// (如 #flag __EXT__ . "name/src/name.c"),符合 phpc 显式模型
}
$extraCFiles = array_unique($extraCFiles);
}
// Process #flag directives (filter by platform + compiler)
if (!empty($allFlags)) {
$platformMap = ['Windows' => 'Windows', 'Linux' => 'Linux', 'Darwin' => 'Darwin', 'MacOS' => 'Darwin'];
$currentOS = PHP_OS_FAMILY;
// $ccClass 已在编译器选择阶段计算(条件编译共用)
// Allowed #flag prefixes (whitelist — blocks arbitrary flag injection)
$allowedFlagPrefixes = [
'-I', '-L', '-l', '-D', '-U',
'-O0', '-O1', '-O2', '-O3', '-Os', '-Og', '-Ofast',
'-Wall', '-Wextra', '-Wpedantic', '-Werror', '-W', '-w',
'-std', '-m', '-f', '-g', '-pthread', '-static', '-shared',
'-B', // TCC library path
'-include', // force-include header before other processing (GCC/Clang/TCC)
];
foreach ($allFlags as $f) {
$pf = $f['platform'] ?? '';
$cf = $f['compiler'] ?? '';
$flagsStr = $f['flags'] ?? '';
$platformOk = ($pf === '' || ($platformMap[$pf] ?? '') === $currentOS);
$compilerOk = ($cf === '' || $cf === $ccClass);
if (!$platformOk || !$compilerOk) continue;
// Security: block shell metacharacters (prevent command injection)
if (preg_match('/[`$|;&><\n\r\\\\]/', $flagsStr)) {
die("Error: #flag '{$flagsStr}' contains unsafe shell characters (backtick, $, |, ;, &, >, <, \\n, \\, newline)\n");
}
// Security: blacklist dangerous flag patterns
// -fplugin=/path → GCC 插件可执行任意代码
// -specs=/path → GCC specs 文件可注入任意命令
// -wrapper → 包装器可执行任意命令
// -ld= → 链接器替换
if (preg_match('/-fplugin\s*=?|-specs\s*=?|-wrapper\s|-ld\s*=/', $flagsStr)) {
die("Error: #flag '{$flagsStr}' contains a blacklisted flag (-fplugin/-specs/-wrapper/-ld are not allowed for security)\n");
}
// Security: validate each individual flag token against whitelist
$tokens = preg_split('/\s+/', trim($flagsStr));
// macOS framework 链接:-framework X → -Wl,-framework,X
// TCC 不识别 -framework 语法(会把 X 当作输入文件),需通过 -Wl, 透传给系统 ld
$fwTokens = [];
for ($ti = 0; $ti < count($tokens); $ti++) {
if ($tokens[$ti] === '-framework' && isset($tokens[$ti + 1])) {
$fwTokens[] = '-Wl,-framework,' . $tokens[$ti + 1];
$ti++;
} elseif ($tokens[$ti] === '-F' && isset($tokens[$ti + 1])) {
// framework 搜索路径同理:-F path → -Wl,-F,path
$fwTokens[] = '-Wl,-F,' . $tokens[$ti + 1];
$ti++;
} else {
$fwTokens[] = $tokens[$ti];
}
}
$tokens = $fwTokens;
foreach ($tokens as $tok) {
if ($tok === '' || $tok === '-') continue;
// .c 文件:加入 extraCFiles(由编译器编译),不混入 extraFlags
if (str_ends_with($tok, '.c')) {
$cPath = realpath($tok);
if ($cPath === false) {
die("Error: #flag '.c' file not found: {$tok}\n");
}
$extraCFiles[] = $cPath;
continue;
}
// Non-flag values (file paths, raw numbers) — always allowed
if (!str_starts_with($tok, '-')) {
$extraFlags .= ' ' . $tok;
continue;
}
// Check against whitelist
$allowed = false;
foreach ($allowedFlagPrefixes as $pfx) {
if (str_starts_with($tok, $pfx)) { $allowed = true; break; }
}
if (!$allowed) {
die("Error: #flag '{$tok}' is not in the allowed list. Allowed prefixes: " . implode(', ', $allowedFlagPrefixes) . "\n");
}
// Security: resolve -I and -L paths via realpath (prevents traversal via ..)
if ((str_starts_with($tok, '-I') || str_starts_with($tok, '-L')) && strlen($tok) > 2) {
$path = substr($tok, 2);
$resolved = realpath($path);
if ($resolved === false) {
die("Error: #flag '{$tok}' path does not exist: {$path}\n");
}
$extraFlags .= ' ' . $tok[0] . $tok[1] . '"' . $resolved . '"';
continue;
}
$extraFlags .= ' ' . $tok;
}
}
}
// 默认 -O2:GCC/Clang 自动加,TCC 不加(TCC 无优化级别)
$ccLower = $cc !== null ? strtolower($cc) : '';
if ((str_contains($ccLower, 'gcc') || str_contains($ccLower, 'clang'))
&& !str_contains($extraFlags, '-O')) {
$extraFlags .= ' -O2';
}
// MinGW GCC workaround: math.h functions may not be declared
if (PHP_OS_FAMILY === 'Windows' && str_contains($ccLower, 'gcc')) {
$extraFlags .= ' -Wno-implicit-function-declaration -Wno-int-conversion -Wno-discarded-qualifiers';
}
// 分离 -L/-l/-Wl, 到 linkFlags:链接器单遍扫描,库必须在 .c 文件之后
// (TCC/Unix 链接器对顺序敏感;-L/-l 放在源文件之前会导致 unresolved reference)
// -Wl, 透传链接器选项(如 macOS -framework),同样需放在源文件之后
$lateLinkFlags = '';
$extraFlagTokens = preg_split('/\s+/', trim($extraFlags));
$keptFlags = [];
foreach ($extraFlagTokens as $tok) {
if ($tok === '') continue;
if (str_starts_with($tok, '-L') || str_starts_with($tok, '-l') || str_starts_with($tok, '-Wl,')) {
$lateLinkFlags .= ' ' . $tok;
} else {
$keptFlags[] = $tok;
}
}
$extraFlags = !empty($keptFlags) ? ' ' . implode(' ', $keptFlags) : '';
if (!is_dir($outDir)) mkdir($outDir, 0777, true);
// Phase 1.5: Type Check — 填充 AST 节点的 inferredType 字段
// 使 CodeGenerator 能基于类型信息生成泛型数组等优化代码
try {
$checker = new TypeChecker(new SymbolTable());
$checker->check($merged);
} catch (\Throwable $e) {
// TypeChecker 错误不阻塞编译(CodeGenerator 有回退逻辑)
if ($debugMode) {
fwrite(STDERR, "[WARN] TypeChecker: " . $e->getMessage() . "\n");
}
}
// ── 代码生成阶段 ──
// --ssa 模式:FlatAst → SSA → 优化 → C
// 默认模式:ProgramNode → CodeGenerator → C
if ($ssaMode) {
if ($debugMode) echo "[*] SSA mode enabled\n";
// 1. Node AST → FlatAst
$converter = new FlatAstConverter();
$flatAst = $converter->convert($merged);
// 2. FlatAst → SSAModule(遍历 ProgramNode 子节点,构建每个 FunctionNode 的 SSA)
$ssaModule = new SSAModule();
$programIdx = $flatAst->root;
$childCount = $flatAst->childCount($programIdx);
for ($i = 0; $i < $childCount; $i++) {
$childIdx = $flatAst->child($programIdx, $i);
if ($flatAst->nodes[$childIdx]['kind'] === NodeKind::FunctionNode) {
$builder = new SSABuilder();
$ssaFunc = $builder->build($flatAst, $childIdx);
// 用 newFunction 创建占位项后替换为实际 SSAFunction
// (SSABuilder.build 内部已设置 entryBlockId / values / blocks)
$fid = $ssaModule->newFunction($ssaFunc->name, $ssaFunc->paramTypes, $ssaFunc->retType);
$ssaModule->functions[$fid] = $ssaFunc;
}
}
// 3. SSA 优化(对每个函数运行 fixpoint 优化)
$optPass = new SSAOptPass();
foreach ($ssaModule->functions as $ssaFunc) {
$optPass->runUntilFixpoint($ssaFunc);
}
// 4. SSA → C 降低
$toC = new SSAToCGenerator();
$cCode = $toC->generate($ssaModule, $entryFile);
if (!is_dir($outDir)) mkdir($outDir, 0777, true);
$cFile = $outDir . DIRECTORY_SEPARATOR . pathinfo($entryFile, PATHINFO_FILENAME) . '.c';
file_put_contents($cFile, $cCode);
} else {
$gen = new CodeGenerator();
$gen->isShared = $isShared;
$cFile = $gen->generate($merged, $entryFile, $outDir);
}
echo " [YES] {$cFile}\n";
} catch (\Throwable $e) {
fwrite(STDERR, "[NO] Transpile failed: " . $e->getMessage() . "\n" . $e->getTraceAsString() . "\n");
exit(1);
}
// --- Phase 2: C compile → binary ---
echo "[2/2] Compiling => {$outExe}...\n";
// TCC -B flag: computed after cross-compilation so we know the final compiler
$bFlag = '';
$tccLibDir = '';
// ── Cross-compilation ─────────────────────────────
if ($targetOS !== null) {
$currentOS = strtolower(PHP_OS_FAMILY); // windows|linux|darwin
if ($targetOS === $currentOS) {
echo "[*] -os {$targetOS} -arch {$targetArch} == current, native compile\n";
} else {
echo "[*] Cross-compile: {$currentOS} → {$targetOS}/{$targetArch}\n";
// Platform defines
$platformDefines = [
'windows' => '-D_WIN32 -DWIN32',
'linux' => '-D__linux__ -D__linux',
'darwin' => '-D__APPLE__ -D__MACH__',
];
if (isset($platformDefines[$targetOS])) {
$extraFlags .= ' ' . $platformDefines[$targetOS];
}
// Cross-compiler auto-detection
// Priority: 1. clang -target (native cross-compile) 2. GCC triplet
if ($cc === null) {
$triplets = [
'windows' . $targetArch => "{$targetArch}-windows-gnu",
'linux' . $targetArch => "{$targetArch}-linux-gnu",
'darwin' . $targetArch => "{$targetArch}-apple-darwin",
];
$targetTriple = $triplets[$targetOS . $targetArch] ?? '';
$found = null;
// 1st: try system clang with -target (works from any platform)
foreach (['clang', 'clang-19', 'clang-18', 'clang-17'] as $clangBin) {
exec("\"{$clangBin}\" --version 2>&1", $vOut, $vRet);
if ($vRet === 0) {
$found = "{$clangBin} -target {$targetTriple}";
break;
}
}
// 2nd: try GCC cross-compiler triplets
if ($found === null) {
$gccTriplets = [
'windows' => ["{$targetArch}-w64-mingw32-", 'i686-w64-mingw32-'],
'linux' => ["{$targetArch}-linux-gnu-"],
'darwin' => ["{$targetArch}-apple-darwin-"],
];
$candidates = $gccTriplets[$targetOS] ?? [];
if ($targetArch === 'x86_64') {
$candidates = array_merge($candidates, $gccTriplets[$targetOS] ?? []);
}
foreach (array_unique($candidates) as $prefix) {
foreach (['gcc', 'clang'] as $suffix) {
$testCC = $prefix . $suffix;
exec("\"{$testCC}\" --version 2>&1", $vOut, $vRet);
if ($vRet === 0) { $found = $testCC; break 2; }
exec("where \"{$testCC}\" 2>nul", $wOut, $wRet);
if ($wRet === 0) { $found = $testCC; break 2; }
}
}
}
if ($found !== null) {
$cc = $found;
// Separate binary from flags: "clang -target xxx" → ccExe=clang, extraFlags+=-target xxx
if (str_contains($found, ' ')) {
[$ccBinary, $ccArgs] = explode(' ', $found, 2);
$ccExe = $ccBinary;
$extraFlags = $ccArgs . ' ' . $extraFlags;
} else {
$ccExe = $found;
}
echo "[*] Auto-detected cross-compiler: {$found}\n";
} else {
$installHints = [
'windows' => [
'Linux' => ' apt install clang mingw-w64',
'Darwin' => ' brew install llvm mingw-w64',
'Windows' => ' winget install LLVM.LLVM',
],
'linux' => [
'Darwin' => ' brew install llvm',
'Windows' => ' winget install LLVM.LLVM',
'Linux' => '',
],
'darwin' => [
'Linux' => ' apt install clang lld',
'Windows' => ' Unsupported (macOS requires Apple SDK)',
'Darwin' => '',
],
];
$hint = $installHints[$targetOS][PHP_OS_FAMILY] ?? '';
die("Error: no cross-compiler (clang/gcc) found for '{$targetOS}'.\n\n"
. "Install LLVM/clang (recommended) or MinGW-w64:\n"
. ($hint ? "{$hint}\n\n" : "\n")
. "Or specify manually: -cc <compiler> -os {$targetOS}\n"
. "Example: -cc x86_64-w64-mingw32-gcc -os windows\n");
}
}
}
// Platform-specific output extension
if ($isShared) {
// -shared 模式:动态库扩展名
$shExt = ($targetOS === 'windows' || ($targetOS === null && PHP_OS_FAMILY === 'Windows')) ? '.dll'
: (($targetOS === 'darwin' || ($targetOS === null && PHP_OS_FAMILY === 'Darwin')) ? '.dylib' : '.so');
if (str_ends_with($outExe, '.exe')) $outExe = substr($outExe, 0, -4);
if (!str_ends_with($outExe, $shExt)) $outExe .= $shExt;
} elseif ($targetOS === 'windows' && !str_ends_with($outExe, '.exe')) {
$outExe .= '.exe';
} elseif ($targetOS !== 'windows' && str_ends_with($outExe, '.exe')) {
$outExe = substr($outExe, 0, -4);
}
}
// Now compute TCC-specific flags (after cross-compilation may have changed $cc)
$ccLower = $cc !== null ? strtolower($cc) : '';
$isTCC = ($cc === null || str_contains($ccLower, 'tcc'));
if ($isTCC && $inPhar) {
if (PHP_OS_FAMILY === 'Windows') {
$tccSysDir = $pharDir . DIRECTORY_SEPARATOR . 'tcc' . DIRECTORY_SEPARATOR . 'win32';
} elseif (PHP_OS_FAMILY !== 'Darwin') {
$tccSysDir = $pharDir . DIRECTORY_SEPARATOR . 'tcc';
}
if (isset($tccSysDir) && is_dir($tccSysDir)) {
// build.sh puts libtcc1.a & headers at tcc/lib/tcc/
$tccLibDir = $tccSysDir . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'tcc';
$bFlag = ' -B"' . (is_dir($tccLibDir) ? $tccLibDir : $tccSysDir) . '"';
// -nostdinc: 禁止搜索系统 /usr/include,防止与 PHAR 内打包的 glibc 头文件冲突
$tccIncDir = $tccLibDir . DIRECTORY_SEPARATOR . 'include';
if (is_dir($tccIncDir)) {
$bFlag .= ' -nostdinc -I"' . $tccIncDir . '"';
// Linux: 追加系统 include 路径作为补充
// TCC 自带 glibc 替代头文件优先(-I 顺序在前),系统路径只补充
// TCC 没有的开发库头文件(X11/Wayland/OpenGL/GTK 等)。
// 这样用户 #include <X11/Xlib.h> 等系统开发库头时可被找到。
if (PHP_OS_FAMILY !== 'Windows' && PHP_OS_FAMILY !== 'Darwin') {
foreach (['/usr/local/include', '/usr/include'] as $sysInc) {
if (is_dir($sysInc)) {
$bFlag .= ' -I"' . $sysInc . '"';
}
}
// 多架构子目录(Debian/Ubuntu: /usr/include/x86_64-linux-gnu 等)
// 提供 asm/ioctls.h 等内核 ABI 头文件,TCC 自带 bits/ioctls.h 是
// 桩文件需 include <asm/ioctls.h>,但 asm/ 在 multiarch 子目录下。
// Arch/Fedora 的 asm/ 直接在 /usr/include/asm/,已被上面覆盖。
foreach (glob('/usr/include/*/asm') as $asmDir) {
$bFlag .= ' -I"' . dirname($asmDir) . '"';
}
}
}
}
} elseif ($isTCC) {
// Dev mode: auto-detect TCC standalone directory
if (PHP_OS_FAMILY !== 'Darwin') {
$tccBase = dirname($ccExe);
// build.sh puts libtcc1.a at tcc/lib/tcc/ — match that path
$libDir = $tccBase . '/lib/tcc';
if (is_dir($libDir) && file_exists($libDir . '/libtcc1.a')) {
$bFlag = ' -B"' . realpath($libDir) . '"';
} else {
foreach ([$tccBase . '/tcc-standalone', $tccBase] as $dir) {
if (is_dir($dir . '/lib') || is_dir($dir . '/include')) {