feat(阶段5): 让 bun build --compile 的产物真正自足
编译产物拿到没有 node_modules 的目录里跑,原来是一路崩的 —— 而且**在仓库
目录里跑时全都正常**,因为它顺着 cwd 找到了 node_modules,假装没事。
这类问题只会在服务器上第一次启动时暴露。逐个堵掉:
- `import.meta.dir` 在二进制里恒为 `/$bunfs/root`,往上三级就是文件系统根:
`data/test_case` 悄悄变成 `/data/test_case`,`.env` 去读 `/.env`。
新增 runtime.ts 显式分叉:编译后按 cwd,开发时按仓库根(后者不能改,
compose 挂给判题沙箱的是仓库根的 data/test_case)。
- sql.js / tree-sitter 的 wasm、jieba 的 .node 和 4.8MB 词典,
原来都靠运行时 require.resolve / Bun.resolveSync / __dirname 去 node_modules 里找。
全部改成 `with { type: "file" }` 内嵌成资源。jieba 尤其绕:它的 index.js
运行时探测平台再 require 子包,dict.js 用 __dirname 读 dict.txt,两条都
依赖磁盘布局,所以单独包了 vendor/jieba.ts 直接 require 内嵌的 .node。
- SQL 判题要 spawn 一个能被 SIGKILL 的子进程,原来 spawn 的是 child.ts 的路径,
编译后那个文件不存在。改成二进制自己按 argv 分发:新增 main.ts 作为唯一入口,
serve / worker / sql-child 三个子命令,镜像里只需要一份运行时。
## 顺带修掉一个能拖垮生产的坑
「spawn 自己」意味着只要 argv 分发这一环出问题(子命令改名没同步、compose 里
command 写错、拿别的入口编了二进制),「起自己」就变成「把整个程序再跑一遍」,
而那一遍又会 spawn 一个自己 —— 指数增长。
这不是假想:开发时用一个没有分发器的临时入口编了个二进制,一跑就递归 fork
105MB 的进程,几秒触发 global OOM,内核把开发机的终端杀了
(`selftest invoked oom-killer ... Killed process ... Alacritty`)。
同样的错误发生在服务器上就是判题机连同数据库一起拖死。
加了递归闸:父进程 spawn 时打 OJ2_SQL_CHILD 标记,带标记的进程一律拒绝再 spawn,
最多一层就停在一条明确的 SYSTEM_ERROR 上。
## 验证
产物拷进只有它自己一个文件的目录,2G 内存上限下跑,7 项全过:
jieba 分词(自定义词「循环结构」「死循环」命中)、tree-sitter Python3/C 各两条
(含一条**预期失败**的规则做对照,否则「语言没加载成功」和「规则通过」返回值
一模一样,分不出来)、SQL 判题 AC、SQL 只读防护仍拦住 PRAGMA。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,22 @@ import { randomBytes } from "node:crypto"
|
||||
import { readFileSync } from "node:fs"
|
||||
import { isAbsolute, resolve } from "node:path"
|
||||
|
||||
import { isCompiled, pathBase } from "./runtime"
|
||||
|
||||
/**
|
||||
* Bun 只自动加载「当前工作目录」下的 .env。而本应用的启动方式(`bun run --filter '@oj2/api' dev`)
|
||||
* 会把 cwd 切到 apps/api/,于是仓库根的 .env 读不到 —— 而 .env.example 恰恰教人写在根目录。
|
||||
* 这里显式补读仓库根的 .env,让文档指引真正生效,且不管从哪个目录启动都一致。
|
||||
*
|
||||
* 只填充尚未设置的键:真实环境变量与 cwd 下的 .env 优先级更高,不被覆盖。
|
||||
*
|
||||
* 编译成单二进制后不做这件事:生产靠 compose 注入环境变量,而 `import.meta.dir` 在
|
||||
* 二进制里是 `/$bunfs/root`,往上三级会去读 `/.env` —— 读到什么都是意外。
|
||||
*/
|
||||
function loadRepoRootEnv() {
|
||||
if (isCompiled) return
|
||||
try {
|
||||
const text = readFileSync(resolve(import.meta.dir, "../../../.env"), "utf8")
|
||||
const text = readFileSync(resolve(pathBase, ".env"), "utf8")
|
||||
for (const line of text.split("\n")) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith("#")) continue
|
||||
@@ -35,15 +41,12 @@ loadRepoRootEnv()
|
||||
* 也不要在仓库里写死一个人人都知道的弱默认值。
|
||||
*/
|
||||
/**
|
||||
* 相对路径一律按**仓库根**解析,而不是进程 cwd。
|
||||
* 相对路径按 `pathBase` 解析(开发时是仓库根,编译后是 cwd),基准的取舍见 runtime.ts。
|
||||
*
|
||||
* 起服务的方式(`bun run --filter '@oj2/api' dev`)会把 cwd 切到 apps/api/,
|
||||
* 于是 "data/test_case" 落在 apps/api/data/ 下 —— 而 docker/compose.dev.yml 把
|
||||
* 仓库根的 data/test_case 挂进判题沙箱。两边不是同一个目录,新传的测试点判题时
|
||||
* 会「找不到测试数据」,而且只在真正判题时才暴露。
|
||||
* 生产环境**应当**用绝对路径的环境变量把这些目录显式指定掉,相对路径只是开发便利。
|
||||
*/
|
||||
function repoPath(value: string) {
|
||||
return isAbsolute(value) ? value : resolve(import.meta.dir, "../../..", value)
|
||||
return isAbsolute(value) ? value : resolve(pathBase, value)
|
||||
}
|
||||
|
||||
function judgeServerToken() {
|
||||
|
||||
Reference in New Issue
Block a user