@actiondock/builder 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/package.json +41 -0
- package/src/archive.ts +248 -0
- package/src/compiler.ts +303 -0
- package/src/errors.ts +67 -0
- package/src/exporter.ts +407 -0
- package/src/index.ts +5 -0
- package/src/planner.ts +453 -0
- package/src/types.ts +262 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @actiondock/builder
|
|
2
|
+
|
|
3
|
+
ActionDock 2.0 构建规划、编译器调度与技能导出包。
|
|
4
|
+
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
|
+
|
|
9
|
+
`@actiondock/builder` 负责静态分析依赖关系、调度外部 Bun 编译器管线以及将工具和规程打包导出为可分发的 Agent Skill 资产。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 核心组件与能力
|
|
14
|
+
|
|
15
|
+
### BuildPlanner 依赖闭包计算器
|
|
16
|
+
|
|
17
|
+
通过纯文本与声明式元数据静态分析依赖关系,无需执行任何用户 TypeScript 业务代码,杜绝模块加载过程中的副作用:
|
|
18
|
+
|
|
19
|
+
- **清单事实源驱动**:读取 `actiondock.manifest.json` 与 Playbook 的 YAML 头部,精确提取 Actions 与 Playbooks 映射。
|
|
20
|
+
- **递归依赖闭包**:自顶向下递归解析 Action 的 `uses` 声明列表,构建完整的静态调用图。
|
|
21
|
+
- **规程驱动按需剪枝**:支持针对特定 Playbook 进行定向打包,自动计算并仅保留该 Playbook 直接或间接调用的最小 Action 集合,实现 Tree-shaking 瘦身。
|
|
22
|
+
- **静态资产收集**:自动识别并关联清单中声明的外部静态资产文件。
|
|
23
|
+
|
|
24
|
+
### BunCompiler 外部编译器调度器
|
|
25
|
+
|
|
26
|
+
封装对外部 Bun 编译器的调度,将 Action Package 编译为目标平台的单文件独立二进制产物:
|
|
27
|
+
|
|
28
|
+
- **前置参数严密校验**:在发起编译前严格校验入口文件有效性、输出目录权限、字节码编译标志与代码混淆压缩参数。
|
|
29
|
+
- **跨平台目标架构适配**:支持指定跨平台交叉编译目标架构,覆盖 `linux-x64`、`linux-arm64`、`darwin-x64`、`darwin-arm64`、`windows-x64` 等主流平台。
|
|
30
|
+
- **编译器调用与错误归一化**:通过外部进程安全调度 `bun build --compile`,捕获编译器标准错误输出并转换为结构化错误代码。
|
|
31
|
+
- **构建元数据提取**:自动计算产物文件的字节大小、耗时以及 SHA-256 校验和。
|
|
32
|
+
|
|
33
|
+
### SkillExporter 技能导出器
|
|
34
|
+
|
|
35
|
+
将原子 Action 与操作规程 Playbook 打包导出为面向主流 AI 智能体生态的标准 Agent Skill 资产:
|
|
36
|
+
|
|
37
|
+
- **生成 SKILL.md 文档**:提取 Playbook SOP 内容与工具元数据,合成符合智能体规范的操作指南文档与声明头部。
|
|
38
|
+
- **源码模式技能导出**:保留 TypeScript 源码与依赖声明,适合在具备 Node.js 运行环境的智能体容器中直接运行。
|
|
39
|
+
- **独立二进制模式技能导出**:内嵌经过编译的单文件可执行产物,智能体宿主无需安装 Node.js 或 Bun 即可开箱即用。
|
|
40
|
+
- **标准压缩归档**:支持将导出的技能目录无缝归档打包为 `.zip` 或 `.tar.gz` 压缩格式,便于分发上传。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 编程调用示例
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { BuildPlanner, BunCompiler, SkillExporter } from "@actiondock/builder";
|
|
48
|
+
|
|
49
|
+
// 计算依赖规划闭包
|
|
50
|
+
const planner = new BuildPlanner({ projectRoot: "/path/to/project" });
|
|
51
|
+
const plan = planner.createPlan({ playbookId: "review-pr" });
|
|
52
|
+
|
|
53
|
+
// 调度外部编译器
|
|
54
|
+
const compiler = new BunCompiler();
|
|
55
|
+
const buildResult = await compiler.compile({
|
|
56
|
+
entrypoint: plan.entrypoint,
|
|
57
|
+
outfile: "./dist/review-pr-tool",
|
|
58
|
+
minify: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 导出便携式技能
|
|
62
|
+
const exporter = new SkillExporter({ projectRoot: "/path/to/project" });
|
|
63
|
+
const exportResult = await exporter.export({
|
|
64
|
+
outputDir: "./dist/skills",
|
|
65
|
+
playbookId: "review-pr",
|
|
66
|
+
standalone: true,
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 开源协议
|
|
73
|
+
|
|
74
|
+
本项目采用 Apache-2.0 开源协议。
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actiondock/builder",
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "ActionDock Build Planner, Bun standalone compiler, and Skill exporter",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"module": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./src/index.ts",
|
|
12
|
+
"types": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.12.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bun test"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@actiondock/core": "^2.0.0",
|
|
31
|
+
"@actiondock/sdk": "^2.0.0",
|
|
32
|
+
"yaml": "^2.7.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"typescript": "^5.7.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": ["actiondock", "builder", "compiler", "skill", "export"],
|
|
39
|
+
"author": "team4u",
|
|
40
|
+
"license": "Apache-2.0"
|
|
41
|
+
}
|
package/src/archive.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { deflateRawSync, gzipSync } from "node:zlib";
|
|
3
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 纯 Node 实现的归档压缩模块。
|
|
7
|
+
*
|
|
8
|
+
* zip 与 tar.gz 均在进程内完成打包,不依赖宿主机的 zip / tar 命令行工具
|
|
9
|
+
* (Windows 与最小化 Linux 环境普遍缺少 zip 命令)。
|
|
10
|
+
* 压缩能力复用 node:zlib 内置的 deflateRaw(zip 条目)与 gzip(tar.gz 容器)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 目录内相对路径条目(目录在前、同级按名称排序,保证归档内容顺序稳定) */
|
|
14
|
+
interface ArchiveEntry {
|
|
15
|
+
/** 相对根目录的 POSIX 风格路径(目录以 / 结尾仅出现在 zip 条目名层面) */
|
|
16
|
+
relPath: string;
|
|
17
|
+
/** 是否为目录 */
|
|
18
|
+
isDir: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 递归收集目录内全部条目 */
|
|
22
|
+
function collectEntries(dir: string, baseDir = dir): ArchiveEntry[] {
|
|
23
|
+
const entries: ArchiveEntry[] = [];
|
|
24
|
+
for (const name of readdirSync(dir).sort()) {
|
|
25
|
+
const fullPath = join(dir, name);
|
|
26
|
+
const stat = statSync(fullPath);
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: true });
|
|
29
|
+
entries.push(...collectEntries(fullPath, baseDir));
|
|
30
|
+
} else if (stat.isFile()) {
|
|
31
|
+
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: false });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return entries;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ------------------------------------------------------------------ */
|
|
38
|
+
/* zip 格式写入 */
|
|
39
|
+
/* ------------------------------------------------------------------ */
|
|
40
|
+
|
|
41
|
+
/** CRC32 查表(IEEE 802.3 多项式) */
|
|
42
|
+
const CRC32_TABLE: Uint32Array = (() => {
|
|
43
|
+
const table = new Uint32Array(256);
|
|
44
|
+
for (let n = 0; n < 256; n++) {
|
|
45
|
+
let c = n;
|
|
46
|
+
for (let k = 0; k < 8; k++) {
|
|
47
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
48
|
+
}
|
|
49
|
+
table[n] = c >>> 0;
|
|
50
|
+
}
|
|
51
|
+
return table;
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
function crc32(buf: Buffer): number {
|
|
55
|
+
let crc = 0xffffffff;
|
|
56
|
+
for (let i = 0; i < buf.length; i++) {
|
|
57
|
+
crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
58
|
+
}
|
|
59
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Unix 毫秒时间戳转 DOS 时间格式(date/time 各 16 位) */
|
|
63
|
+
function dosDateTime(ms: number): { date: number; time: number } {
|
|
64
|
+
const d = new Date(ms);
|
|
65
|
+
const year = Math.max(d.getFullYear(), 1980);
|
|
66
|
+
const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
|
|
67
|
+
const date = ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
|
|
68
|
+
return { date, time };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** central directory 记录(写入 EOCD 前聚合) */
|
|
72
|
+
interface ZipCentralRecord {
|
|
73
|
+
header: Buffer;
|
|
74
|
+
localOffset: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 将目录打包为标准 zip 归档(PKZIP APPNOTE 结构)。
|
|
79
|
+
*
|
|
80
|
+
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
81
|
+
* @param outPath 输出 zip 文件路径
|
|
82
|
+
*/
|
|
83
|
+
export function createZipArchive(dir: string, outPath: string): void {
|
|
84
|
+
const rootName = basename(dir);
|
|
85
|
+
const entries = collectEntries(dir);
|
|
86
|
+
const chunks: Buffer[] = [];
|
|
87
|
+
const centralRecords: ZipCentralRecord[] = [];
|
|
88
|
+
let offset = 0;
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
// 归档内路径带根目录前缀;目录条目名以 / 结尾
|
|
92
|
+
const name = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
93
|
+
const nameBuf = Buffer.from(name, "utf8");
|
|
94
|
+
const stat = statSync(join(dir, entry.relPath));
|
|
95
|
+
const { date, time } = dosDateTime(stat.mtimeMs);
|
|
96
|
+
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(join(dir, entry.relPath));
|
|
97
|
+
|
|
98
|
+
let method = 0;
|
|
99
|
+
let payload = content;
|
|
100
|
+
if (!entry.isDir && content.length > 0) {
|
|
101
|
+
const deflated = deflateRawSync(content, { level: 9 });
|
|
102
|
+
if (deflated.length < content.length) {
|
|
103
|
+
method = 8;
|
|
104
|
+
payload = deflated;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const crc = crc32(content);
|
|
109
|
+
|
|
110
|
+
// Local File Header(30 字节 + 文件名)
|
|
111
|
+
const local = Buffer.alloc(30);
|
|
112
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
113
|
+
local.writeUInt16LE(20, 4);
|
|
114
|
+
local.writeUInt16LE(0x0800, 6); // UTF-8 文件名标志
|
|
115
|
+
local.writeUInt16LE(method, 8);
|
|
116
|
+
local.writeUInt16LE(time, 10);
|
|
117
|
+
local.writeUInt16LE(date, 12);
|
|
118
|
+
local.writeUInt32LE(crc, 14);
|
|
119
|
+
local.writeUInt32LE(payload.length, 18);
|
|
120
|
+
local.writeUInt32LE(content.length, 22);
|
|
121
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
122
|
+
local.writeUInt16LE(0, 28);
|
|
123
|
+
chunks.push(local, nameBuf, payload);
|
|
124
|
+
|
|
125
|
+
// Central Directory Header(46 字节 + 文件名)
|
|
126
|
+
const central = Buffer.alloc(46);
|
|
127
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
128
|
+
central.writeUInt16LE(20, 4); // version made by
|
|
129
|
+
central.writeUInt16LE(20, 6); // version needed
|
|
130
|
+
central.writeUInt16LE(0x0800, 8);
|
|
131
|
+
central.writeUInt16LE(method, 10);
|
|
132
|
+
central.writeUInt16LE(time, 12);
|
|
133
|
+
central.writeUInt16LE(date, 14);
|
|
134
|
+
central.writeUInt32LE(crc, 16);
|
|
135
|
+
central.writeUInt32LE(payload.length, 20);
|
|
136
|
+
central.writeUInt32LE(content.length, 24);
|
|
137
|
+
central.writeUInt16LE(nameBuf.length, 28);
|
|
138
|
+
central.writeUInt16LE(0, 30); // extra len
|
|
139
|
+
central.writeUInt16LE(0, 32); // comment len
|
|
140
|
+
central.writeUInt16LE(0, 34); // disk start
|
|
141
|
+
central.writeUInt16LE(0, 36); // internal attrs
|
|
142
|
+
// 外部属性:Unix 权限左移 16 位,目录附加 MS-DOS 目录位
|
|
143
|
+
const extAttrs = ((entry.isDir ? 0o40755 : 0o100644) << 16) | (entry.isDir ? 0x10 : 0);
|
|
144
|
+
central.writeUInt32LE(extAttrs >>> 0, 38);
|
|
145
|
+
central.writeUInt32LE(offset, 42);
|
|
146
|
+
centralRecords.push({ header: Buffer.concat([central, nameBuf]), localOffset: offset });
|
|
147
|
+
|
|
148
|
+
offset += local.length + nameBuf.length + payload.length;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const centralDirOffset = offset;
|
|
152
|
+
let centralDirSize = 0;
|
|
153
|
+
for (const record of centralRecords) {
|
|
154
|
+
chunks.push(record.header);
|
|
155
|
+
centralDirSize += record.header.length;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// End of Central Directory Record(22 字节)
|
|
159
|
+
const eocd = Buffer.alloc(22);
|
|
160
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
161
|
+
eocd.writeUInt16LE(0, 4);
|
|
162
|
+
eocd.writeUInt16LE(0, 6);
|
|
163
|
+
eocd.writeUInt16LE(centralRecords.length, 8);
|
|
164
|
+
eocd.writeUInt16LE(centralRecords.length, 10);
|
|
165
|
+
eocd.writeUInt32LE(centralDirSize, 12);
|
|
166
|
+
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
167
|
+
eocd.writeUInt16LE(0, 20); // comment len
|
|
168
|
+
chunks.push(eocd);
|
|
169
|
+
|
|
170
|
+
writeFileSync(outPath, Buffer.concat(chunks));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ------------------------------------------------------------------ */
|
|
174
|
+
/* tar.gz 格式写入 */
|
|
175
|
+
/* ------------------------------------------------------------------ */
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 构造单个条目的 USTAR 头块(512 字节)。
|
|
179
|
+
* 路径超长时按 USTAR prefix 字段拆分,仍放不下则抛错。
|
|
180
|
+
*/
|
|
181
|
+
function tarHeader(path: string, size: number, mtimeSec: number, isDir: boolean): Buffer {
|
|
182
|
+
let name = path;
|
|
183
|
+
let prefix = "";
|
|
184
|
+
if (Buffer.byteLength(name, "utf8") > 100) {
|
|
185
|
+
const split = name.lastIndexOf("/");
|
|
186
|
+
if (split <= 0 || split > 155 || name.length - split - 1 > 100) {
|
|
187
|
+
throw new Error(`归档路径超出 USTAR 字段容量: ${path}`);
|
|
188
|
+
}
|
|
189
|
+
prefix = name.slice(0, split);
|
|
190
|
+
name = name.slice(split + 1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const buf = Buffer.alloc(512);
|
|
194
|
+
buf.write(name, 0, 100, "utf8");
|
|
195
|
+
buf.write(isDir ? "0000755" : "0000644", 100, 8, "ascii"); // mode
|
|
196
|
+
buf.write("0000000", 108, 8, "ascii"); // uid
|
|
197
|
+
buf.write("0000000", 116, 8, "ascii"); // gid
|
|
198
|
+
buf.write(size.toString(8).padStart(11, "0"), 124, 12, "ascii"); // size
|
|
199
|
+
buf.write(Math.floor(mtimeSec).toString(8).padStart(11, "0"), 136, 12, "ascii"); // mtime
|
|
200
|
+
buf.fill(" ", 148, 156); // chksum 占位(计算时视为空格)
|
|
201
|
+
buf.write(isDir ? "5" : "0", 156, 1, "ascii"); // typeflag
|
|
202
|
+
buf.write("ustar\0", 257, 6, "ascii"); // magic
|
|
203
|
+
buf.write("00", 263, 2, "ascii"); // version
|
|
204
|
+
if (prefix) buf.write(prefix, 345, 155, "utf8");
|
|
205
|
+
|
|
206
|
+
// 校验和:整块字节的无符号累加和,写作 6 位八进制 + NUL + 空格
|
|
207
|
+
let sum = 0;
|
|
208
|
+
for (let i = 0; i < 512; i++) sum += buf[i];
|
|
209
|
+
buf.write(sum.toString(8).padStart(6, "0"), 148, 7, "ascii");
|
|
210
|
+
buf.write("\0", 154, 1, "ascii");
|
|
211
|
+
buf.write(" ", 155, 1, "ascii");
|
|
212
|
+
return buf;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 将字节数补齐到 512 的整数倍 */
|
|
216
|
+
function pad512(length: number): Buffer {
|
|
217
|
+
const rem = length % 512;
|
|
218
|
+
return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 将目录打包为标准 tar.gz 归档(USTAR + gzip)。
|
|
223
|
+
*
|
|
224
|
+
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
225
|
+
* @param outPath 输出 tar.gz 文件路径
|
|
226
|
+
*/
|
|
227
|
+
export function createTarGzArchive(dir: string, outPath: string): void {
|
|
228
|
+
const rootName = basename(dir);
|
|
229
|
+
const entries = collectEntries(dir);
|
|
230
|
+
const chunks: Buffer[] = [];
|
|
231
|
+
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const path = `${rootName}/${entry.relPath}`;
|
|
234
|
+
const fullPath = join(dir, entry.relPath);
|
|
235
|
+
const stat = statSync(fullPath);
|
|
236
|
+
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(fullPath);
|
|
237
|
+
|
|
238
|
+
chunks.push(tarHeader(path, content.length, Math.floor(stat.mtimeMs / 1000), entry.isDir));
|
|
239
|
+
if (!entry.isDir) {
|
|
240
|
+
chunks.push(content, pad512(content.length));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 归档结尾:两个全零块
|
|
245
|
+
chunks.push(Buffer.alloc(1024));
|
|
246
|
+
|
|
247
|
+
writeFileSync(outPath, gzipSync(Buffer.concat(chunks), { level: 9 }));
|
|
248
|
+
}
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
|
+
import { CompilerError, CompilerValidationError } from "./errors";
|
|
6
|
+
import type { BunCompilerOptions, BunCompilerResult } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 官方支持的 Bun 独立二进制编译目标列表。
|
|
10
|
+
*/
|
|
11
|
+
const SUPPORTED_TARGETS = new Set<string>([
|
|
12
|
+
"bun",
|
|
13
|
+
"host",
|
|
14
|
+
"bun-linux-x64",
|
|
15
|
+
"linux-x64",
|
|
16
|
+
"bun-linux-arm64",
|
|
17
|
+
"linux-arm64",
|
|
18
|
+
"bun-darwin-x64",
|
|
19
|
+
"darwin-x64",
|
|
20
|
+
"bun-darwin-arm64",
|
|
21
|
+
"darwin-arm64",
|
|
22
|
+
"bun-windows-x64",
|
|
23
|
+
"windows-x64",
|
|
24
|
+
"bun-linux-x64-baseline",
|
|
25
|
+
"linux-x64-baseline",
|
|
26
|
+
"bun-linux-x64-modern",
|
|
27
|
+
"linux-x64-modern",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 校验目标架构并规范化。
|
|
32
|
+
*/
|
|
33
|
+
function normalizeAndValidateTarget(target?: string): string {
|
|
34
|
+
if (!target || target === "host" || target === "bun") {
|
|
35
|
+
return "bun";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const cleanTarget = target.trim();
|
|
39
|
+
if (!SUPPORTED_TARGETS.has(cleanTarget)) {
|
|
40
|
+
throw new CompilerValidationError(
|
|
41
|
+
`Unsupported compilation target: '${target}'. Standalone compilation only supports Bun targets (e.g. linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64).`,
|
|
42
|
+
{ code: "UNSUPPORTED_TARGET", target }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cleanTarget.startsWith("bun-") ? cleanTarget : `bun-${cleanTarget}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 规范化编译器错误输出。
|
|
51
|
+
*/
|
|
52
|
+
function normalizeCompilerError(rawStderr: string, rawStdout: string, exitCode: number, target?: string): CompilerError {
|
|
53
|
+
const combined = `${rawStderr}\n${rawStdout}`.trim();
|
|
54
|
+
const lines = combined
|
|
55
|
+
.split(/\r?\n/)
|
|
56
|
+
.map((l) => l.trim())
|
|
57
|
+
.filter((l) => l.length > 0);
|
|
58
|
+
|
|
59
|
+
let code = "COMPILATION_FAILED";
|
|
60
|
+
if (combined.includes("Unsupported target")) {
|
|
61
|
+
code = "UNSUPPORTED_TARGET";
|
|
62
|
+
} else if (
|
|
63
|
+
combined.includes("Cannot find package") ||
|
|
64
|
+
combined.includes("Cannot find module") ||
|
|
65
|
+
combined.includes("Could not resolve")
|
|
66
|
+
) {
|
|
67
|
+
code = "MODULE_NOT_FOUND";
|
|
68
|
+
} else if (
|
|
69
|
+
combined.includes("SyntaxError") ||
|
|
70
|
+
combined.includes("Parse error") ||
|
|
71
|
+
combined.includes("Expected") ||
|
|
72
|
+
combined.includes("Unexpected")
|
|
73
|
+
) {
|
|
74
|
+
code = "SYNTAX_ERROR";
|
|
75
|
+
} else if (combined.includes("EACCES") || combined.includes("Permission denied")) {
|
|
76
|
+
code = "PERMISSION_DENIED";
|
|
77
|
+
} else if (combined.includes("ENOSPC") || combined.includes("no space left")) {
|
|
78
|
+
code = "NO_SPACE";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const summaryLine = lines.find((l) => l.startsWith("error:")) || lines[0] || "Bun compilation failed";
|
|
82
|
+
|
|
83
|
+
return new CompilerError(
|
|
84
|
+
`Bun compilation failed with exit code ${exitCode} (${code}): ${summaryLine}`,
|
|
85
|
+
{
|
|
86
|
+
code,
|
|
87
|
+
exitCode,
|
|
88
|
+
rawError: combined,
|
|
89
|
+
details: lines,
|
|
90
|
+
target,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Bun 独立二进制编译器。
|
|
97
|
+
* 封装 bun build --compile 调用,提前校验选项组合,收集构建产物与元数据。
|
|
98
|
+
*/
|
|
99
|
+
export class BunCompiler {
|
|
100
|
+
/**
|
|
101
|
+
* 执行独立二进制编译。
|
|
102
|
+
*
|
|
103
|
+
* @param options 编译参数
|
|
104
|
+
* @returns 编译产物元数据结果
|
|
105
|
+
*/
|
|
106
|
+
public async compile(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
|
|
109
|
+
// 1. 入口点存在性校验
|
|
110
|
+
if (!options.entrypoint) {
|
|
111
|
+
throw new CompilerValidationError("Entrypoint must be specified for standalone compilation", {
|
|
112
|
+
code: "INVALID_ENTRYPOINT",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resolvedEntry = resolve(options.entrypoint);
|
|
117
|
+
if (!existsSync(resolvedEntry)) {
|
|
118
|
+
throw new CompilerValidationError(
|
|
119
|
+
`Entrypoint file not found: ${options.entrypoint}`,
|
|
120
|
+
{ code: "ENTRYPOINT_NOT_FOUND" }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. 输出路径校验
|
|
125
|
+
if (!options.outfile) {
|
|
126
|
+
throw new CompilerValidationError("Outfile path must be specified for standalone compilation", {
|
|
127
|
+
code: "INVALID_OUTFILE",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const resolvedOutfile = resolve(options.outfile);
|
|
132
|
+
const outDir = dirname(resolvedOutfile);
|
|
133
|
+
mkdirSync(outDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
// 3. 目标平台与选项校验
|
|
136
|
+
const normalizedTarget = normalizeAndValidateTarget(options.target);
|
|
137
|
+
const isWindowsTarget =
|
|
138
|
+
normalizedTarget.includes("windows") ||
|
|
139
|
+
(!options.target && process.platform === "win32");
|
|
140
|
+
|
|
141
|
+
// 校验布尔选项
|
|
142
|
+
if (options.minify !== undefined && typeof options.minify !== "boolean") {
|
|
143
|
+
throw new CompilerValidationError("Option 'minify' must be a boolean", {
|
|
144
|
+
code: "INVALID_MINIFY_OPTION",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (options.bytecode !== undefined && typeof options.bytecode !== "boolean") {
|
|
148
|
+
throw new CompilerValidationError("Option 'bytecode' must be a boolean", {
|
|
149
|
+
code: "INVALID_BYTECODE_OPTION",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 4. 组装 Bun build 命令参数
|
|
154
|
+
const args = [
|
|
155
|
+
"bun",
|
|
156
|
+
"build",
|
|
157
|
+
resolvedEntry,
|
|
158
|
+
"--compile",
|
|
159
|
+
"--outfile",
|
|
160
|
+
resolvedOutfile,
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
const bytecode = options.bytecode !== false;
|
|
164
|
+
if (bytecode) {
|
|
165
|
+
args.push("--bytecode");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const minify = options.minify !== false;
|
|
169
|
+
if (minify) {
|
|
170
|
+
args.push("--minify");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (normalizedTarget !== "bun") {
|
|
174
|
+
args.push(`--target=${normalizedTarget}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 5. 执行外部编译器进程
|
|
178
|
+
const cwd = options.cwd ? resolve(options.cwd) : dirname(resolvedEntry);
|
|
179
|
+
const proc = spawnSync(args[0], args.slice(1), {
|
|
180
|
+
cwd,
|
|
181
|
+
stdio: "pipe",
|
|
182
|
+
env: {
|
|
183
|
+
...process.env,
|
|
184
|
+
...options.env,
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (proc.error) {
|
|
189
|
+
if ((proc.error as any).code === "ENOENT") {
|
|
190
|
+
throw new CompilerError(
|
|
191
|
+
"Bun binary not found in PATH. Standalone compilation requires Bun to be installed (e.g. via 'curl -fsSL https://bun.sh/install | bash' or 'npm install -g bun').",
|
|
192
|
+
{
|
|
193
|
+
code: "BUN_NOT_FOUND",
|
|
194
|
+
exitCode: 1,
|
|
195
|
+
rawError: proc.error.message,
|
|
196
|
+
target: options.target,
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
throw new CompilerError(`Failed to spawn Bun compiler: ${proc.error.message}`, {
|
|
201
|
+
code: "SPAWN_FAILED",
|
|
202
|
+
exitCode: 1,
|
|
203
|
+
rawError: proc.error.message,
|
|
204
|
+
target: options.target,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (proc.status !== 0) {
|
|
209
|
+
const rawStderr = proc.stderr?.toString("utf-8") || "";
|
|
210
|
+
const rawStdout = proc.stdout?.toString("utf-8") || "";
|
|
211
|
+
throw normalizeCompilerError(rawStderr, rawStdout, proc.status ?? 1, options.target);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 6. 解析生成的可执行二进制物理路径(适配 Windows .exe 后缀特性)
|
|
215
|
+
let actualExecutablePath = resolvedOutfile;
|
|
216
|
+
if (!existsSync(actualExecutablePath) && isWindowsTarget) {
|
|
217
|
+
const withExe = `${resolvedOutfile}.exe`;
|
|
218
|
+
if (existsSync(withExe)) {
|
|
219
|
+
actualExecutablePath = withExe;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!existsSync(actualExecutablePath)) {
|
|
224
|
+
throw new CompilerError(
|
|
225
|
+
`Compiler completed with exit code 0 but output executable not found at ${resolvedOutfile}`,
|
|
226
|
+
{
|
|
227
|
+
code: "OUTPUT_NOT_FOUND",
|
|
228
|
+
exitCode: 0,
|
|
229
|
+
rawError: "Output executable file not found on disk",
|
|
230
|
+
target: options.target,
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 7. 计算二进制大小与校验和
|
|
236
|
+
const binaryBuffer = readFileSync(actualExecutablePath);
|
|
237
|
+
const sizeBytes = statSync(actualExecutablePath).size;
|
|
238
|
+
const sha256 = createHash("sha256").update(binaryBuffer).digest("hex");
|
|
239
|
+
const durationMs = Date.now() - startTime;
|
|
240
|
+
const compiledAt = new Date().toISOString();
|
|
241
|
+
|
|
242
|
+
// 8. 写入 metadata 产物文件
|
|
243
|
+
let metadataPath: string | undefined;
|
|
244
|
+
if (options.emitMetadata !== false) {
|
|
245
|
+
metadataPath = resolve(outDir, "artifact.json");
|
|
246
|
+
const bunVersion =
|
|
247
|
+
(typeof (globalThis as any).Bun !== "undefined" && (globalThis as any).Bun.version) ||
|
|
248
|
+
(() => {
|
|
249
|
+
try {
|
|
250
|
+
const vProc = spawnSync("bun", ["--version"], { stdio: "pipe" });
|
|
251
|
+
if (vProc.status === 0) {
|
|
252
|
+
return vProc.stdout.toString().trim();
|
|
253
|
+
}
|
|
254
|
+
} catch {}
|
|
255
|
+
return "unknown";
|
|
256
|
+
})();
|
|
257
|
+
|
|
258
|
+
const metadata = {
|
|
259
|
+
packageId: options.packageId,
|
|
260
|
+
version: options.version,
|
|
261
|
+
target: options.target || "host",
|
|
262
|
+
executable: basename(actualExecutablePath),
|
|
263
|
+
sizeBytes,
|
|
264
|
+
sha256,
|
|
265
|
+
bunVersion,
|
|
266
|
+
minify,
|
|
267
|
+
bytecode,
|
|
268
|
+
actions: options.actions || [],
|
|
269
|
+
compiledAt,
|
|
270
|
+
};
|
|
271
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
packageId: options.packageId,
|
|
276
|
+
version: options.version,
|
|
277
|
+
target: options.target || "host",
|
|
278
|
+
executablePath: actualExecutablePath,
|
|
279
|
+
sizeBytes,
|
|
280
|
+
sha256,
|
|
281
|
+
metadataPath,
|
|
282
|
+
minify,
|
|
283
|
+
bytecode,
|
|
284
|
+
durationMs,
|
|
285
|
+
compiledAt,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 静态辅助调用方法。
|
|
291
|
+
*/
|
|
292
|
+
public static async compile(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
293
|
+
const compiler = new BunCompiler();
|
|
294
|
+
return compiler.compile(options);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 快捷独立二进制编译函数。
|
|
300
|
+
*/
|
|
301
|
+
export async function compileBinary(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
302
|
+
return BunCompiler.compile(options);
|
|
303
|
+
}
|