diff --git a/.gitignore b/.gitignore index 975d041b..f3d4ca74 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /logs /build /vendor +/node_modules /tmp /projects/wordpress /projects/workerman diff --git a/examples/win32-hello/project.yml b/examples/win32-hello/project.yml index 1babb902..bea763e5 100644 --- a/examples/win32-hello/project.yml +++ b/examples/win32-hello/project.yml @@ -1,6 +1,19 @@ name: win32-hello version: 0.0.1 mode: bin + +# Windows 资源文件配置示例 +# resource: +# icon: icon.ico +# version-info: +# file-version: 0.0.1.0 +# product-version: 0.0.1.0 +# company-name: "My Company" +# file-description: "Win32 Hello World Application" +# legal-copyright: "Copyright (C) 2026 My Company" +# product-name: "Win32 Hello" +# original-filename: "win32-hello.exe" + sources: - hello-win.php - ./cpp-src diff --git a/package.json b/package.json new file mode 100644 index 00000000..a7fbc824 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "compiler", + "version": "1.0.0", + "description": "- 需要 PHP-8.2 以上版本 - 需要 GCC-9 以上版本,支持 C++17 标准 - 需要 CMake-3.24 以上版本", + "main": "index.js", + "directories": { + "doc": "docs", + "example": "examples", + "test": "tests" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git@git.code-galaxy.net:aot/compiler.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "sharp": "^0.34.5" + } +} diff --git a/project.yml b/project.yml index ed70c4cc..26533e7d 100644 --- a/project.yml +++ b/project.yml @@ -5,6 +5,24 @@ cxx-std: c++17 cxx-flags: - -Wall +# Windows 资源文件配置(图标、版本信息等) +# 仅在 Windows 平台编译 bin 模式时生效 +resource: + # 图标文件路径(相对于 project.yml 所在目录,也支持绝对路径) + icon: swoole-logo.ico + # 版本信息 + version-info: + file-version: 0.1.0.1052 + product-version: 0.1.0 + company-name: "上海识沃网络科技有限公司" + file-description: "Swoole AOT Compiler" + internal-name: "swoole-compiler" + legal-copyright: "Copyright (C) 2026 上海识沃网络科技有限公司" + legal-trademarks: "Swoole is a trademark of 上海识沃网络科技有限公司" + original-filename: "swoole_compiler.exe" + product-name: "Swoole Compiler" + comments: "PHP 原生编译器,可将 PHP 项目编译为 Windows/Linux/macOS 平台原生的可执行文件" + sources: - ./src/Php - ./src/Core diff --git a/src/Php/Backend/Msvc.php b/src/Php/Backend/Msvc.php index 4a53a6b4..93769f26 100644 --- a/src/Php/Backend/Msvc.php +++ b/src/Php/Backend/Msvc.php @@ -375,28 +375,50 @@ class Msvc extends CompilerBackend public function buildLinkOptions(array $config = []): string { $cmd = ''; - + // 调试 if (!empty($config['debug'])) { $cmd .= ' /DEBUG'; } - + // Windows 子系统 if (!empty($config['no_console'])) { $cmd .= ' ' . $this->platform->getSubsystemOptions(true); } - + // CRT 配置 $cmd .= ' ' . $this->platform->getCrtConfig(); - + // 扩展模块选项 if (!empty($config['build_mode']) && $config['build_mode'] === 'ext') { $cmd .= ' /DLL'; } - + // nologo $cmd .= ' /nologo'; - + + return $cmd; + } + + /** + * 编译 Windows 资源文件 (.rc) 为目标文件 (.res) + * + * 使用 rc.exe(MSVC 资源编译器)将 .rc 文件编译为 .res 文件 + * .res 文件可以直接传给 link.exe 作为输入 + * + * @param string $rcFile 资源文件路径 (.rc) + * @param string $resFile 输出资源文件路径 (.res) + * @return string 编译命令 + */ + public function compileResourceFile(string $rcFile, string $resFile): string + { + // rc.exe 是 MSVC 自带的资源编译器 + // /nologo: 不显示版权信息 + // /fo: 指定输出文件 + $cmd = 'rc.exe /nologo'; + $cmd .= ' /fo ' . escapeshellarg($resFile); + $cmd .= ' ' . escapeshellarg($rcFile); + return $cmd; } } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 0a25b5dd..720ecbe7 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -19,6 +19,7 @@ use PhpAot\Php\Entity\PropertyDef; use PhpAot\Php\Exception\Redo; use PhpAot\Php\Exception\SyntaxError; use PhpAot\Php\Exception\Unsupported; +use PhpAot\Php\Generator\ResourceFileGenerator; use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Stmt\Foreach_; @@ -39,6 +40,9 @@ class Translator extends Preprocessor protected array $argInfoHeaderFiles = []; protected array $registerSymbols = []; + // Windows 资源文件配置(图标、版本信息等) + protected array $resourceConfig = []; + // 类静态属性初始值 protected array $defaultStaticPropertyList = []; @@ -753,6 +757,9 @@ CODE; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c'; } + // Windows 平台:编译资源文件(图标、版本信息等) + $this->compileResourceFile(); + if (!$this->getPlatform()->supportsPcntlParallelCompile() or $job <= 1) { return $this->compileSourceFile($sourceFiles); } @@ -892,9 +899,115 @@ CODE; $this->climate->{$style}($message); } + // ======================================================================== + // Windows 资源文件支持 + // ======================================================================== + + /** + * 检查是否配置了 Windows 资源信息 + */ + public function hasResourceFile(): bool + { + if (!$this->isWindows()) { + return false; + } + $generator = $this->createResourceGenerator(); + return $generator !== null && $generator->hasResource(); + } + + /** + * 获取 .rc 资源文件路径 + */ + public function getResourceRcFile(): string + { + return $this->getBuildDir() . DIRECTORY_SEPARATOR . 'app_resource.rc'; + } + + /** + * 获取 .res 编译后的资源文件路径 + */ + public function getResourceResFile(): string + { + return $this->getBuildDir() . DIRECTORY_SEPARATOR . 'app_resource.res'; + } + + /** + * 创建资源文件生成器 + */ + protected function createResourceGenerator(): ?ResourceFileGenerator + { + if (empty($this->resourceConfig)) { + return null; + } + $projectDir = $this->resourceConfig['_projectDir'] ?? getcwd(); + return new ResourceFileGenerator($this->resourceConfig, $projectDir); + } + + /** + * 编译 Windows 资源文件 + * + * 如果配置了 resource 选项,生成 .rc 文件并使用 rc.exe 编译为 .res + * .res 文件会在 build 阶段被链接到最终的 exe 中 + */ + protected function compileResourceFile(): void + { + if (!$this->isWindows()) { + return; + } + + $generator = $this->createResourceGenerator(); + if ($generator === null || !$generator->hasResource()) { + return; + } + + // 生成 .rc 文件 + $rcFile = $this->getResourceRcFile(); + $rcContent = $generator->generate(); + // 写入 UTF-8 BOM,确保 rc.exe 正确识别编码,避免中文乱码 + $this->writeFile($rcFile, "\xEF\xBB\xBF" . $rcContent); + $this->climate->info('Generated resource file: ' . $rcFile); + + // 使用 MSVC 的 rc.exe 编译 .rc -> .res + $backend = $this->getCompilerBackend(); + if ($backend instanceof \PhpAot\Php\Backend\Msvc) { + $resFile = $this->getResourceResFile(); + $cmd = $backend->compileResourceFile($rcFile, $resFile); + $this->climate->comment($cmd); + + exec($cmd . ' 2>&1', $output, $ret); + + if (!empty($output)) { + foreach ($output as $line) { + $this->climate->out($line); + } + } + + if ($ret !== 0) { + $this->error('Resource compilation failed: ' . $rcFile); + } + + if (!file_exists($resFile)) { + $this->error('Resource file not generated: ' . $resFile); + } + + $this->climate->green('Resource compiled: ' . $resFile); + } else { + $this->climate->warning('Resource files are only supported with MSVC backend on Windows'); + } + } + public function build(array $objectFiles): void { $targetFile = $this->getTargetFileName(); + + // Windows 平台:将 .res 资源文件加入链接 + if ($this->isWindows() && $this->hasResourceFile()) { + $resFile = $this->getResourceResFile(); + if (file_exists($resFile)) { + $objectFiles[] = $resFile; + } + } + $linkCmd = $this->buildLinkCommand($objectFiles, $targetFile); $this->climate->comment($linkCmd); @@ -1241,6 +1354,26 @@ CODE; } } + // 读取 resource(Windows 资源配置:图标、版本信息等) + $resource = $cfg['resource'] ?? null; + if (!empty($resource)) { + if (!is_array($resource)) { + $this->error('`resource` must be array'); + } + // 验证图标文件是否存在 + if (!empty($resource['icon'])) { + $iconPath = $resource['icon']; + if (!preg_match('/^[A-Za-z]:\\|^\//', $iconPath)) { + $iconPath = $projectDir . DIRECTORY_SEPARATOR . $iconPath; + } + if (!file_exists($iconPath)) { + $this->error('Icon file not exists: `' . $resource['icon'] . '`'); + } + } + $this->resourceConfig = $resource; + $this->resourceConfig['_projectDir'] = $projectDir; + } + return $list; } diff --git a/svg-to-ico.js b/svg-to-ico.js new file mode 100644 index 00000000..4dfa28b0 --- /dev/null +++ b/svg-to-ico.js @@ -0,0 +1,147 @@ +/** + * SVG → ICO 转换脚本 (Node.js) + * + * 使用 sharp 库将 SVG 渲染为多尺寸 PNG,再合并为 ICO + * + * 用法: node svg-to-ico.js [output.ico] + */ + +const sharp = require('sharp'); +const fs = require('fs'); +const path = require('path'); + +const sizes = [16, 32, 48, 64, 128, 256]; + +async function main() { + const args = process.argv.slice(2); + if (args.length < 1) { + console.log('用法: node svg-to-ico.js [output.ico]'); + process.exit(1); + } + + const inputSvg = path.resolve(args[0]); + const outputIco = args.length >= 2 + ? path.resolve(args[1]) + : path.join(path.dirname(inputSvg), path.basename(inputSvg, '.svg') + '.ico'); + + if (!fs.existsSync(inputSvg)) { + console.log(`错误: SVG 文件不存在: ${inputSvg}`); + process.exit(1); + } + + console.log('=== SVG → ICO 转换工具 ===\n'); + console.log(`输入: ${inputSvg}`); + console.log(`输出: ${outputIco}\n`); + + const svgBuffer = fs.readFileSync(inputSvg); + + // 步骤1: 渲染 SVG 为多个尺寸的 PNG + const pngBuffers = {}; + for (const size of sizes) { + console.log(`[1/2] 渲染 SVG → PNG (${size}x${size})...`); + try { + const pngBuffer = await sharp(svgBuffer, { density: 300 }) + .resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 1 } }) + .png() + .toBuffer(); + pngBuffers[size] = pngBuffer; + console.log(` 完成: ${size}x${size} (${pngBuffer.length} bytes)`); + } catch (err) { + console.log(` 警告: ${size}x${size} PNG 生成失败: ${err.message},跳过`); + } + } + + const validSizes = Object.keys(pngBuffers).map(Number).sort((a, b) => a - b); + if (validSizes.length === 0) { + console.log('\n错误: 所有尺寸的 PNG 都生成失败'); + process.exit(1); + } + + // 步骤2: 合并为 ICO + console.log('\n[2/2] 合并 PNG → ICO...'); + const icoBuffer = createIcoFromPngs(pngBuffers, validSizes); + fs.writeFileSync(outputIco, icoBuffer); + + console.log(`\n成功! ICO 文件已生成: ${outputIco}`); + console.log(`包含尺寸: ${validSizes.join(', ')}`); + console.log(`文件大小: ${icoBuffer.length} bytes`); +} + +/** + * 将多个 PNG buffer 合并为 ICO 格式的 Buffer + */ +function createIcoFromPngs(pngBuffers, sizes) { + const imageCount = sizes.length; + + // 计算各部分大小 + const headerSize = 6; // ICO 头部 + const dirEntrySize = 16; // 每个目录条目 + const dirSize = dirEntrySize * imageCount; + + let dataOffset = headerSize + dirSize; + + // 构建目录条目 + const dirEntries = []; + for (const size of sizes) { + const pngData = pngBuffers[size]; + const dataSize = pngData.length; + + // Width/Height: 0 表示 256 + const w = size >= 256 ? 0 : size; + const h = size >= 256 ? 0 : size; + + dirEntries.push({ + width: w, + height: h, + colorCount: 0, + reserved: 0, + planes: 1, + bitCount: 32, + dataSize: dataSize, + offset: dataOffset + }); + + dataOffset += dataSize; + } + + // 计算总大小 + let totalSize = headerSize + dirSize; + for (const size of sizes) { + totalSize += pngBuffers[size].length; + } + + // 构建 ICO 二进制数据 + const buffer = Buffer.alloc(totalSize); + let pos = 0; + + // ICO 头部 (6 bytes) + buffer.writeUInt16LE(0, pos); pos += 2; // Reserved + buffer.writeUInt16LE(1, pos); pos += 2; // Type: 1 = ICO + buffer.writeUInt16LE(imageCount, pos); pos += 2; // Image count + + // 目录条目 + for (const entry of dirEntries) { + buffer.writeUInt8(entry.width, pos); pos += 1; + buffer.writeUInt8(entry.height, pos); pos += 1; + buffer.writeUInt8(entry.colorCount, pos); pos += 1; + buffer.writeUInt8(entry.reserved, pos); pos += 1; + buffer.writeUInt16LE(entry.planes, pos); pos += 2; + buffer.writeUInt16LE(entry.bitCount, pos); pos += 2; + buffer.writeUInt32LE(entry.dataSize, pos); pos += 4; + buffer.writeUInt32LE(entry.offset, pos); pos += 4; + } + + // 图像数据 + for (const size of sizes) { + const pngData = pngBuffers[size]; + pngData.copy(buffer, pos); + pos += pngData.length; + } + + return buffer; +} + +main().catch(err => { + console.error('错误:', err); + process.exit(1); +}); diff --git a/swoole-logo.ico b/swoole-logo.ico new file mode 100644 index 00000000..73e563ce Binary files /dev/null and b/swoole-logo.ico differ diff --git a/swoole-logo.svg b/swoole-logo.svg new file mode 100644 index 00000000..6297cc67 --- /dev/null +++ b/swoole-logo.svg @@ -0,0 +1,15 @@ + + + + Transfon SWOOLE - Registered + + + + \ No newline at end of file diff --git a/version.txt b/version.txt index 6409db25..01ff2b71 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1049 \ No newline at end of file +1051 \ No newline at end of file