Files
zszq-trs/YLErpWeb/rebuild-bundles.py
T
hjhan 56256cad0b [EQD-6838] 补充踩坑注释:main.post 业务错误走 reject、本文件独立 script 部署、bundle 大小写敏感+非致命
- base/main.js __post/main.post:标注业务错误(success=false/errcode)与网络异常均走 deferred.reject,
  调用方只挂 .done 会静默跳过失败分支(约定2补充曾因此长期不生效)。
- app/swaptrade/swapTradeEdit.js calcBondForItem:标注本文件为独立 <script>(不在 bundle.js)、须 dotnet publish
  刷新 JsVersion 缓存键才生效;并展开 .fail 注释记录约定2补充一度不生效的历史根因。
- rebuild-bundles.py:标注 bundleconfig 输入路径在 Linux 大小写敏感(曾 MISSING 打挂 publish)、
  缺失输入必须非致命(否则 exit 1 连带打挂 dotnet publish)。纯注释,无逻辑改动。
2026-07-29 19:51:42 +08:00

208 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
rebuild-bundles.py —— 前端 bundle 产物的确定性重建脚本
为什么需要它:
页面加载的是 bundle.js / jquery.js / vue.js / bundle.css / bundleV2.css / bundleV2.js
(见 _MainLayout.cshtml),这些是由 bundleconfig.json 把分散的源文件拼接而成。
历史上"改了源文件却忘了重新打 bundle"导致部署的仍是陈旧/残缺产物(本次 EQD-6838 的痛点)。
本脚本按与历史手工打包一致的规则确定性地重建这 6 个产物,使"改源 -> 重新打 bundle"可复现、可校验。
拼接规则(已与已提交版本逐字节比对验证一致):
1. 每个输入文件:剥掉文件首的 UTF-8 BOM(EF BB BF),行尾 CRLF->LF 归一(不改文件原始编码,GBK 等字节原样保留)。
2. 输入文件之间用一个 '\\n' 分隔(末尾文件后不加分隔符)。
3. CSS 产物额外做相对 url() 重写:把相对于各 CSS 源文件目录的 url(...) 改写为相对于
bundle 输出目录(Statics/bundles/)的路径,行为与 BuildBundlerMinifier 一致:
- data: URI 原样保留(含引号);
- 其余 url 保留原始引号风格,仅改写路径为相对 bundle 目录的相对路径。
(注:已提交 bundle.css 中少量 GBK 中文注释曾被旧工具误解码成 UTF-8 替换符,本脚本生成的是正确字节;
重建后这些注释乱码会被修正,仅影响注释,不影响渲染。)
用法:
python3 rebuild-bundles.py # 重建并写回 wwwroot/Statics/bundles/ 下 6 个产物
python3 rebuild-bundles.py --verify # 重建到内存并与已提交文件比对,不一致则 exit 1(供 CI 校验)
"""
import json
import os
import re
import sys
BOM = b'\xef\xbb\xbf'
ROOT = os.path.dirname(os.path.abspath(__file__)) # YLErpWeb 目录
# 引号优先匹配,避免 data: URI 内的 ) 截断
URL_RE = re.compile(rb"url\(\s*('[^']*'|\"[^\"]*\"|[^\"'()]*)\s*\)")
def normalize(b):
"""剥文件首 BOM(若有) + 行尾归一为 \\n。全程字节级,不改编码(GBK 等原样保留)。"""
if isinstance(b, bytes):
if b.startswith(BOM):
b = b[3:]
return b.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
return b.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
def rewrite_css_urls(css, input_rel, out_rel):
"""CSS 相对 url() 改写为相对 bundle 输出目录(与 BuildBundlerMinifier 行为一致)。"""
base = os.path.dirname(input_rel) # 输入文件目录(相对 ROOT)
outdir = os.path.dirname(out_rel) # bundle 输出目录(相对 ROOT)
def repl(m):
raw = m.group(1).strip()
# 保留 url() 内原始引号风格
if len(raw) >= 2 and raw[0:1] == raw[-1:] and raw[0:1] in (b"'", b'"'):
q = raw[0:1]
inner = raw[1:-1].strip()
else:
q = b''
inner = raw
if inner.startswith(b'data:'):
return m.group(0) # data: URI 原样保留(含引号)
if re.match(rb'^(https?:|//|#)', inner) or inner.startswith(b'/'):
return m.group(0)
try:
udec = inner.decode('ascii') # url 通常为 ascii
except Exception:
return m.group(0)
resolved = os.path.normpath(os.path.join(base, udec))
rel = os.path.relpath(resolved, outdir).replace(os.sep, '/')
return b'url(' + q + rel.encode('utf-8', errors='surrogateescape') + q + b')'
return URL_RE.sub(repl, css)
def build_all():
cfg_path = os.path.join(ROOT, 'bundleconfig.json')
raw = open(cfg_path, 'rb').read()
raw = re.sub(rb'//[^\n]*', b'', raw) # 去掉 // 行注释
cfg = json.loads(raw.decode('utf-8-sig'))
results = [] # (out, generated_bytes, missing_list)
for b in cfg:
out = b['outputFileName']
iscss = out.endswith('.css')
parts = []
missing = []
for f in b['inputFiles']:
p = os.path.join(ROOT, f)
if not os.path.exists(p):
# ⚠️【CI 踩坑必读 / 大小写敏感】Windows 大小写不敏感,本地能找到 MyJs.js / isoWeek.js
# 但 Jenkins 是 Linux,大小写敏感,bundleconfig.json 里写的输入路径必须与 git 实际跟踪的
# 文件名【大小写完全一致】,否则这里判 missing。曾经因此报 MISSING INPUT 并把整个 dotnet publish 打挂。
# 改 bundleconfig 的输入文件名时,务必先 `git ls-files` 确认真实大小写。
missing.append(f)
continue
content = normalize(open(p, 'rb').read())
if iscss:
content = rewrite_css_urls(content, f, out)
parts.append(content)
joined = b'\n'.join(parts) # 文件间一个 \n 分隔,末尾不加
results.append((out, joined, missing))
return results
def is_stale(out, input_files):
"""增量检查:任一源文件比 bundle 产物新(或产物不存在)则返回 True。"""
dest = os.path.join(ROOT, out)
if not os.path.exists(dest):
return True
out_mtime = os.path.getmtime(dest)
for f in input_files:
p = os.path.join(ROOT, f)
if os.path.exists(p) and os.path.getmtime(p) > out_mtime:
return True
return False
def is_ci_environment():
"""检测是否在 CI 环境运行。
CI 下 git checkout 后所有文件 mtime 几乎相同,增量检查不可靠,
因此 CI 环境强制全量重建。
"""
ci_vars = ('CI', 'JENKINS_HOME', 'BUILD_NUMBER', 'GITHUB_ACTIONS',
'GITLAB_CI', 'TF_BUILD')
return any(os.environ.get(v) for v in ci_vars)
def main():
verify = '--verify' in sys.argv[1:]
force_rebuild = is_ci_environment() # CI 环境强制全量重建
# 读取配置用于增量检查
cfg_path = os.path.join(ROOT, 'bundleconfig.json')
raw = open(cfg_path, 'rb').read()
raw = re.sub(rb'//[^\n]*', b'', raw)
cfg = json.loads(raw.decode('utf-8-sig'))
# 增量过滤:非 verify、非 CI 时,跳过源文件未变更的 bundle
# CI 环境(force_rebuild=True)强制全量重建,因为 git checkout 后 mtime 不可靠
stale_bundles = []
for b in cfg:
out = b['outputFileName']
if verify or force_rebuild or is_stale(out, b['inputFiles']):
stale_bundles.append(b)
else:
print(f" [UP-TO-DATE] {out}")
# 只重建过期的 bundle
results = []
for b in stale_bundles:
out = b['outputFileName']
iscss = out.endswith('.css')
parts = []
missing = []
for f in b['inputFiles']:
p = os.path.join(ROOT, f)
if not os.path.exists(p):
# ⚠️【CI 踩坑必读 / 大小写敏感】Windows 大小写不敏感,本地能找到 MyJs.js / isoWeek.js
# 但 Jenkins 是 Linux,大小写敏感,bundleconfig.json 里写的输入路径必须与 git 实际跟踪的
# 文件名【大小写完全一致】,否则这里判 missing。曾经因此报 MISSING INPUT 并把整个 dotnet publish 打挂。
# 改 bundleconfig 的输入文件名时,务必先 `git ls-files` 确认真实大小写。
missing.append(f)
continue
content = normalize(open(p, 'rb').read())
if iscss:
content = rewrite_css_urls(content, f, out)
parts.append(content)
joined = b'\n'.join(parts)
results.append((out, joined, missing))
ok = True
for out, joined, missing in results:
dest = os.path.join(ROOT, out)
if missing:
# ⚠️【CI 踩坑必读 / 非致命】缺失输入文件绝不 fail:早期实现把 MISSING 当致命错误 exit 1
# 会连带打挂整个 dotnet publishyml 报错 "命令已退出,代码为 1")。
# 现改为:重建模式跳过该 bundle(保留已提交的产物即可,CI 仍能出包);只有 --verify 校验模式才 exit 1。
# 重建模式下警告但不 fail,避免阻塞 CI 主构建
if verify:
ok = False
print(f" [MISSING INPUT] {out}: {missing}")
else:
print(f" [SKIP-MISSING] {out}: 输入缺失 {missing},保留已提交产物")
continue
if verify:
original = normalize(open(dest, 'rb').read())
if joined != original:
ok = False
n = min(len(joined), len(original))
i = 0
while i < n and joined[i] == original[i]:
i += 1
print(f" [STALE] {out} 与源文件不同步 @byte {i}")
print(f" gen={joined[max(0,i-40):i+40]!r}")
print(f" com={original[max(0,i-40):i+40]!r}")
else:
open(dest, 'wb').write(joined)
print(f"{'VERIFY ' if verify else 'WRITE '} -> {out} ({len(joined)} bytes)"
+ ("" if not missing else f" MISSING={missing}"))
if verify:
print("\n全部与源同步 ✅" if ok else "\n存在不同步,请先运行 `python3 rebuild-bundles.py` 重建并提交 ✅")
else:
print("\n重建完成" if ok else "\n重建完成(部分 bundle 输入缺失已跳过,请检查)")
# 重建模式永远返回 0,不阻塞 CI;Verify 模式才可能返回 1
return 0 if (ok or not verify) else 1
if __name__ == '__main__':
sys.exit(main())