feat: 实现 Windows 可选字体包安装链
This commit is contained in:
@@ -1,3 +1,67 @@
|
||||
!ifdef FONT_PACK_COMPANION_FILE
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
!include "FileFunc.nsh"
|
||||
!include "nsDialogs.nsh"
|
||||
|
||||
Var fontPackCompanionAvailable
|
||||
Var fontPackCompanionCheckbox
|
||||
Var fontPackCompanionExitCode
|
||||
Var fontPackCompanionHash
|
||||
Var fontPackCompanionPath
|
||||
Var fontPackCompanionSelected
|
||||
|
||||
!macro customInit
|
||||
StrCpy $fontPackCompanionAvailable "0"
|
||||
StrCpy $fontPackCompanionSelected "0"
|
||||
StrCpy $fontPackCompanionPath "$EXEDIR\${FONT_PACK_COMPANION_FILE}"
|
||||
IfFileExists "$fontPackCompanionPath" 0 font_pack_init_done
|
||||
${StdUtils.HashFile} $fontPackCompanionHash "SHA2-256" "$fontPackCompanionPath"
|
||||
StrCmp $fontPackCompanionHash "${FONT_PACK_COMPANION_SHA256}" 0 font_pack_init_done
|
||||
StrCpy $fontPackCompanionAvailable "1"
|
||||
IfSilent font_pack_silent font_pack_interactive
|
||||
font_pack_interactive:
|
||||
StrCpy $fontPackCompanionSelected "1"
|
||||
Goto font_pack_init_done
|
||||
font_pack_silent:
|
||||
${GetParameters} $R0
|
||||
${GetOptions} $R0 "/WITHFONTPACK=" $R1
|
||||
StrCmp $R1 "1" 0 font_pack_init_done
|
||||
StrCpy $fontPackCompanionSelected "1"
|
||||
font_pack_init_done:
|
||||
!macroend
|
||||
|
||||
Function FontPackCompanionPage
|
||||
StrCmp $fontPackCompanionAvailable "1" 0 font_pack_page_skip
|
||||
nsDialogs::Create 1018
|
||||
Pop $R0
|
||||
StrCmp $R0 "error" 0 +2
|
||||
Abort
|
||||
${NSD_CreateLabel} 0 0 100% 28u "检测到经过完整性校验的可选字体包。安装后,政企公文主题可使用与 PDF 相同的原生中文字体资产。"
|
||||
Pop $R0
|
||||
${NSD_CreateCheckbox} 0 38u 100% 14u "安装 ${FONT_PACK_COMPANION_NAME}"
|
||||
Pop $fontPackCompanionCheckbox
|
||||
${NSD_Check} $fontPackCompanionCheckbox
|
||||
nsDialogs::Show
|
||||
Return
|
||||
font_pack_page_skip:
|
||||
Abort
|
||||
FunctionEnd
|
||||
|
||||
Function FontPackCompanionPageLeave
|
||||
${NSD_GetState} $fontPackCompanionCheckbox $R0
|
||||
StrCmp $R0 ${BST_CHECKED} 0 font_pack_page_unchecked
|
||||
StrCpy $fontPackCompanionSelected "1"
|
||||
Return
|
||||
font_pack_page_unchecked:
|
||||
StrCpy $fontPackCompanionSelected "0"
|
||||
FunctionEnd
|
||||
|
||||
!macro customPageAfterChangeDir
|
||||
Page custom FontPackCompanionPage FontPackCompanionPageLeave
|
||||
!macroend
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro customInstall
|
||||
WriteRegStr SHCTX \
|
||||
"Software\Classes\MarkdownPdfExporter.Markdown" \
|
||||
@@ -43,6 +107,24 @@
|
||||
"" \
|
||||
"$INSTDIR\md-to-pdf.exe"
|
||||
StrCpy $launchLink "$INSTDIR\md-to-pdf.exe"
|
||||
!ifdef FONT_PACK_COMPANION_FILE
|
||||
StrCmp $fontPackCompanionAvailable "1" 0 font_pack_install_done
|
||||
StrCmp $fontPackCompanionSelected "1" 0 font_pack_install_done
|
||||
${StdUtils.HashFile} $fontPackCompanionHash "SHA2-256" "$fontPackCompanionPath"
|
||||
StrCmp $fontPackCompanionHash "${FONT_PACK_COMPANION_SHA256}" 0 font_pack_integrity_failed
|
||||
DetailPrint "正在安装可选字体包:${FONT_PACK_COMPANION_NAME}"
|
||||
ExecWait '"$fontPackCompanionPath" /S' $fontPackCompanionExitCode
|
||||
StrCmp $fontPackCompanionExitCode "0" font_pack_install_done
|
||||
DetailPrint "可选字体包安装失败,退出码:$fontPackCompanionExitCode"
|
||||
IfSilent font_pack_install_done 0
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "主程序已安装,但可选字体包安装失败(退出码 $fontPackCompanionExitCode)。您可以稍后单独运行字体包安装器。"
|
||||
Goto font_pack_install_done
|
||||
font_pack_integrity_failed:
|
||||
DetailPrint "可选字体包完整性复核失败,已跳过安装。"
|
||||
IfSilent font_pack_install_done 0
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "可选字体包完整性复核失败,主程序已安装,但字体包已跳过。"
|
||||
font_pack_install_done:
|
||||
!endif
|
||||
System::Call 'shell32::SHChangeNotify(i 0x08000000, i 0, i 0, i 0)'
|
||||
!macroend
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ const windowsIcon = path.resolve(
|
||||
__dirname,
|
||||
"../../logos/desktop/windows/app.ico"
|
||||
);
|
||||
const nsisInclude = process.env.MD_TO_PDF_NSIS_INCLUDE?.trim() ||
|
||||
"build/installer.nsh";
|
||||
|
||||
module.exports = {
|
||||
appId: "com.md-to-pdf.desktop",
|
||||
@@ -71,7 +73,7 @@ module.exports = {
|
||||
installerIcon: windowsIcon,
|
||||
uninstallerIcon: windowsIcon,
|
||||
installerLanguages: ["zh_CN"],
|
||||
include: "build/installer.nsh",
|
||||
include: nsisInclude,
|
||||
artifactName: `md-to-pdf-${version}-x86_64-Setup.\${ext}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,6 +58,28 @@ describe("桌面发行构建链", () => {
|
||||
expect(builderConfig).toContain('to: "samples"');
|
||||
});
|
||||
|
||||
it("仅在组合构建时接入经过哈希校验的同目录字体包", () => {
|
||||
const builderConfig = readFileSync(
|
||||
`${desktopRoot}/electron-builder.config.cjs`,
|
||||
"utf8"
|
||||
);
|
||||
const installerInclude = readFileSync(
|
||||
`${desktopRoot}/build/installer.nsh`,
|
||||
"utf8"
|
||||
);
|
||||
const companionBuild = readFileSync(
|
||||
`${projectRoot}/scripts/build-desktop-with-font-pack.mjs`,
|
||||
"utf8"
|
||||
);
|
||||
|
||||
expect(builderConfig).toContain("MD_TO_PDF_NSIS_INCLUDE");
|
||||
expect(installerInclude).toContain("FONT_PACK_COMPANION_SHA256");
|
||||
expect(installerInclude).toContain("StdUtils.HashFile");
|
||||
expect(installerInclude).toContain("/WITHFONTPACK=");
|
||||
expect(companionBuild).toContain("createDesktopCompanionInclude");
|
||||
expect(companionBuild).toContain("MD_TO_PDF_NSIS_INCLUDE");
|
||||
});
|
||||
|
||||
it("将 DOCX Lua Filter 复制到桌面主进程资源目录", () => {
|
||||
const buildScript = readFileSync(
|
||||
`${desktopRoot}/scripts/build-main.mjs`,
|
||||
|
||||
+27
-2
@@ -323,6 +323,29 @@ FP3 只处理独立 NSIS 字体包、Docker 可选挂载及其安装/升级/卸
|
||||
本阶段未修改 NSIS 或 Docker;下一步 FP3-B 实现独立用户级字体包安装、
|
||||
升级、卸载和主安装器同目录可选入口。
|
||||
|
||||
阶段 12D-FP3-B 已实现 Windows 独立字体包安装与 Desktop 同目录可选入口。
|
||||
字体包构建器复用 electron-builder `26.15.3` 锁定并校验的 NSIS 3.0.4.1,
|
||||
不要求系统安装 NSIS,也不引入第二套 Electron。独立安装器以当前用户权限
|
||||
将完整 `mdtp-serif-sc` ID 安装到
|
||||
`%LOCALAPPDATA%/md-to-pdf/font-packs/`,不注册系统字体;安装使用暂存、
|
||||
备份、原子提升和失败回滚,同版本重装幂等,独立卸载只清理该 ID。隔离
|
||||
根目录验收中,首次静默安装、同版本重装和卸载退出码均为 0,注册器复核
|
||||
的字体包指纹仍为
|
||||
`67adac137fa74ca54b4ab07dd584ae87657a1aec413b7f125141c5351fa5b89d`。
|
||||
|
||||
主安装器通过生成的 NSIS include 绑定同目录字体包安装器的精确文件名与
|
||||
SHA-256;交互模式仅在文件存在且哈希正确时显示默认勾选项,并在执行前
|
||||
再次复核哈希。静默主安装默认跳过,只有 `/WITHFONTPACK=1` 才安装;字体
|
||||
包失败不会回滚主程序。统一组合命令为
|
||||
`npm run desktop:make:with-font-pack`,普通 Desktop 构建不启用伴随逻辑。
|
||||
真实组合产物中,主安装器为 115,328,133 字节,独立字体包安装器为
|
||||
24,489,062 字节;桌面解包目录未包含字体二进制或字体包安装器。Windows
|
||||
实机安装向导确认可选页位于安装目录页之后,说明文字完整且字体包默认
|
||||
勾选;未执行主程序重装。全仓 465 项测试、类型检查、生产构建和组合发行
|
||||
构建通过。下一步 FP3-C 实现 Docker 可选字体包挂载与部署门禁。
|
||||
本阶段按版本策略未提前修改应用的 `0.5.1` 版本号;正式 `v0.6.0` 发布
|
||||
提交升级版本后,字体包才满足其 `minimumAppVersion=0.6.0` 运行时门禁。
|
||||
|
||||
## 2. 已完成
|
||||
|
||||
### 2.1 项目骨架
|
||||
@@ -1279,8 +1302,10 @@ ECharts 第二阶段浏览器与 PDF 验证结果:
|
||||
- 阶段 12D-FP2:已完成 Preview/PDF WOFF2 与 DOCX 静态 TrueType 的
|
||||
同源字体解析、跨端受限资源入口、缓存指纹、降级诊断和真实字体包验证;
|
||||
- 阶段 12D-FP3-A:已完成独立字体包构建器、冻结配方、许可证与来源追踪、
|
||||
同版本不可变门禁和真实资产构建;下一步 FP3-B 实现独立 NSIS 字体包,
|
||||
FP3-C 再实现 Docker 可选挂载,最终由 R4 完成 14 套主题深度视觉回归;
|
||||
同版本不可变门禁和真实资产构建;
|
||||
- 阶段 12D-FP3-B:已完成独立用户级 NSIS 字体包、原子安装/升级/卸载、
|
||||
主安装器同目录哈希校验和显式静默开关;下一步 FP3-C 实现 Docker 可选
|
||||
挂载,最终由 R4 完成 14 套主题深度视觉回归;
|
||||
- 阶段 13:完成 Word/WPS 双向互存、外部主题兼容、体积和正式发布验收。
|
||||
|
||||
每个阶段验收通过后创建一个独立提交,再进入下一阶段。当前阶段不得混入
|
||||
|
||||
@@ -45,3 +45,52 @@ root/<pack-id>/<semver>/font-pack.json
|
||||
当前 `mdtp-serif-sc@1.0.0` 只替换 `FandolSong` 与
|
||||
`Mdpdf Fandol Song` 的 400/700 字重,不匹配 `SimSun` 等通用别名,避免
|
||||
错误替换仿宋、黑体、楷体或普通外部主题字体。
|
||||
|
||||
## Windows 独立安装器
|
||||
|
||||
发布人员可以使用 electron-builder 锁定并校验的 NSIS 工具链构建独立
|
||||
字体包安装器,不需要额外安装系统 NSIS:
|
||||
|
||||
```powershell
|
||||
npm run build:font-pack:windows
|
||||
```
|
||||
|
||||
默认产物为:
|
||||
|
||||
```text
|
||||
output/font-packs/windows/
|
||||
└── md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe
|
||||
```
|
||||
|
||||
安装器使用当前用户权限,将整个 `mdtp-serif-sc` ID 安装到:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%/md-to-pdf/font-packs/mdtp-serif-sc/1.0.0/
|
||||
```
|
||||
|
||||
它不向 Windows 系统字体目录注册字体。同一版本重装保持幂等,升级时先把
|
||||
该 ID 的现有目录移入备份,再原子提升新版本;失败会回滚。独立卸载项只
|
||||
删除安装器拥有的 `mdtp-serif-sc` ID,不影响其他字体包 ID。安装器支持
|
||||
`/S` 静默安装和卸载。
|
||||
|
||||
## Desktop 伴随发行
|
||||
|
||||
组合构建命令为:
|
||||
|
||||
```powershell
|
||||
npm run desktop:make:with-font-pack
|
||||
```
|
||||
|
||||
该命令把主安装器和字体包安装器作为两个同目录文件发行。主安装器只编译
|
||||
字体包文件名与 SHA-256,不嵌入字体二进制;仅当同目录文件存在且哈希完全
|
||||
一致时,交互安装向导才显示默认勾选的字体包选项。字体包失败不回滚已安装
|
||||
的主程序。
|
||||
|
||||
主安装器静默运行时默认不安装伴随字体包;只有显式传入以下参数才会安装:
|
||||
|
||||
```powershell
|
||||
md-to-pdf-<version>-x86_64-Setup.exe /S /WITHFONTPACK=1
|
||||
```
|
||||
|
||||
普通 `npm run make -w @md-to-pdf/desktop` 不启用伴随逻辑,避免本地残留的
|
||||
生成文件影响常规桌面构建。
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
Unicode true
|
||||
RequestExecutionLevel user
|
||||
SetCompressor /SOLID lzma
|
||||
SetCompressorDictSize 64
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
!ifndef PACK_DIRECTORY
|
||||
!error "PACK_DIRECTORY is required"
|
||||
!endif
|
||||
!ifndef PACK_ID
|
||||
!error "PACK_ID is required"
|
||||
!endif
|
||||
!ifndef PACK_VERSION
|
||||
!error "PACK_VERSION is required"
|
||||
!endif
|
||||
!ifndef PACK_NAME
|
||||
!error "PACK_NAME is required"
|
||||
!endif
|
||||
!ifndef PACK_LICENSE_FILE
|
||||
!error "PACK_LICENSE_FILE is required"
|
||||
!endif
|
||||
!ifndef OUTPUT_FILE
|
||||
!error "OUTPUT_FILE is required"
|
||||
!endif
|
||||
!ifndef FONT_PACK_ROOT
|
||||
!define FONT_PACK_ROOT "$LOCALAPPDATA\md-to-pdf\font-packs"
|
||||
!endif
|
||||
!ifndef MANAGER_ROOT
|
||||
!define MANAGER_ROOT "$LOCALAPPDATA\md-to-pdf\font-pack-installers"
|
||||
!endif
|
||||
|
||||
Name "Markdown PDF 导出器字体包 - ${PACK_NAME}"
|
||||
OutFile "${OUTPUT_FILE}"
|
||||
InstallDir "${MANAGER_ROOT}\${PACK_ID}"
|
||||
ShowInstDetails show
|
||||
ShowUninstDetails show
|
||||
BrandingText "Markdown PDF 导出器"
|
||||
|
||||
!ifdef ICON_FILE
|
||||
Icon "${ICON_FILE}"
|
||||
UninstallIcon "${ICON_FILE}"
|
||||
!define MUI_ICON "${ICON_FILE}"
|
||||
!define MUI_UNICON "${ICON_FILE}"
|
||||
!endif
|
||||
|
||||
!define MUI_ABORTWARNING
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_LICENSE "${PACK_LICENSE_FILE}"
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
|
||||
VIProductVersion "1.0.0.0"
|
||||
VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "ProductName" "Markdown PDF 导出器字体包"
|
||||
VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "FileDescription" "${PACK_NAME}"
|
||||
VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "FileVersion" "${PACK_VERSION}"
|
||||
VIAddVersionKey /LANG=${LANG_SIMPCHINESE} "LegalCopyright" "Copyright © YIXIONG Tech.ltd"
|
||||
|
||||
Section "安装字体包" SEC_FONT_PACK
|
||||
SetShellVarContext current
|
||||
StrCpy $R0 "${FONT_PACK_ROOT}\.installing-${PACK_ID}"
|
||||
StrCpy $R1 "${FONT_PACK_ROOT}\${PACK_ID}"
|
||||
StrCpy $R2 "${FONT_PACK_ROOT}\.backup-${PACK_ID}"
|
||||
|
||||
CreateDirectory "${FONT_PACK_ROOT}"
|
||||
RMDir /r "$R0"
|
||||
RMDir /r "$R2"
|
||||
CreateDirectory "$R0\${PACK_ID}\${PACK_VERSION}"
|
||||
SetOutPath "$R0\${PACK_ID}\${PACK_VERSION}"
|
||||
File /r "${PACK_DIRECTORY}\*.*"
|
||||
SetOutPath "$TEMP"
|
||||
IfFileExists "$R0\${PACK_ID}\${PACK_VERSION}\font-pack.json" +3 0
|
||||
RMDir /r "$R0"
|
||||
Abort "字体包暂存校验失败:缺少 font-pack.json"
|
||||
|
||||
ClearErrors
|
||||
IfFileExists "$R1\*.*" 0 promote_pack
|
||||
Rename "$R1" "$R2"
|
||||
IfErrors install_failed
|
||||
|
||||
promote_pack:
|
||||
ClearErrors
|
||||
Rename "$R0\${PACK_ID}" "$R1"
|
||||
IfErrors rollback_pack
|
||||
RMDir /r "$R0"
|
||||
RMDir /r "$R2"
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
FileOpen $R3 "$INSTDIR\installed-version.txt" w
|
||||
FileWrite $R3 "${PACK_VERSION}$\r$\n"
|
||||
FileClose $R3
|
||||
|
||||
!ifdef REGISTER_UNINSTALL
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "DisplayName" "Markdown PDF 导出器字体包 - ${PACK_NAME}"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "DisplayVersion" "${PACK_VERSION}"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "Publisher" "YIXIONG Tech.ltd"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
|
||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "NoModify" 1
|
||||
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}" "NoRepair" 1
|
||||
!endif
|
||||
Goto install_done
|
||||
|
||||
rollback_pack:
|
||||
RMDir /r "$R1"
|
||||
IfFileExists "$R2\*.*" 0 install_failed
|
||||
Rename "$R2" "$R1"
|
||||
|
||||
install_failed:
|
||||
RMDir /r "$R0"
|
||||
Abort "字体包安装失败,原版本已保留。"
|
||||
|
||||
install_done:
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
SetShellVarContext current
|
||||
RMDir /r "${FONT_PACK_ROOT}\${PACK_ID}"
|
||||
RMDir /r "${FONT_PACK_ROOT}\.installing-${PACK_ID}"
|
||||
RMDir /r "${FONT_PACK_ROOT}\.backup-${PACK_ID}"
|
||||
!ifdef REGISTER_UNINSTALL
|
||||
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\md-to-pdf-font-pack-${PACK_ID}"
|
||||
!endif
|
||||
Delete "$INSTDIR\installed-version.txt"
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
SectionEnd
|
||||
Generated
+1
-1
@@ -5631,7 +5631,6 @@
|
||||
"integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.15.3",
|
||||
"builder-util": "26.15.3",
|
||||
@@ -10037,6 +10036,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"app-builder-lib": "26.15.3",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"desktop:package": "npm run package -w @md-to-pdf/desktop",
|
||||
"build:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node packages/font-pack-builder/dist/cli.js build",
|
||||
"verify:font-pack": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-theme-engine && npm run build -w @md-to-pdf/font-pack-registry && npm run build -w @md-to-pdf/docx-engine && npm run build -w @md-to-pdf/font-pack-builder && node packages/font-pack-builder/dist/cli.js verify",
|
||||
"build:font-pack:windows": "npm run verify:font-pack && node packages/font-pack-builder/dist/cli.js windows-installer",
|
||||
"desktop:make:with-font-pack": "npm run build:font-pack:windows && node scripts/build-desktop-with-font-pack.mjs",
|
||||
"theme:import-typora": "node scripts/import-typora-theme.mjs",
|
||||
"verify:docx-reference": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:pandoc -w @md-to-pdf/docx-engine",
|
||||
"verify:docx-conversion": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/docx-engine && npm run verify:conversion -w @md-to-pdf/docx-engine",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"app-builder-lib": "26.15.3",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { resolve } from "node:path";
|
||||
import { buildFontPack, verifyFontPack } from "./builder.js";
|
||||
import { buildWindowsFontPackInstaller } from "./windows-installer.js";
|
||||
|
||||
function argumentsMap(values: string[]) {
|
||||
const result = new Map<string, string>();
|
||||
@@ -61,7 +62,31 @@ async function main() {
|
||||
packVersion: option(values, "pack-version", "1.0.0")
|
||||
});
|
||||
}
|
||||
throw new Error("命令必须是 build 或 verify");
|
||||
if (command === "windows-installer") {
|
||||
return buildWindowsFontPackInstaller({
|
||||
packRoot: resolve(option(values, "root", "output/font-packs/root")),
|
||||
appVersion: option(values, "app-version", "0.6.0"),
|
||||
packId: option(values, "pack-id", "mdtp-serif-sc"),
|
||||
packVersion: option(values, "pack-version", "1.0.0"),
|
||||
scriptPath: resolve(
|
||||
option(values, "script", "font-packs/windows/font-pack-installer.nsi")
|
||||
),
|
||||
outputDirectory: resolve(
|
||||
option(values, "output", "output/font-packs/windows")
|
||||
),
|
||||
iconPath: resolve(
|
||||
option(values, "icon", "logos/desktop/windows/app.ico")
|
||||
),
|
||||
reportPath: resolve(
|
||||
option(
|
||||
values,
|
||||
"report",
|
||||
"output/font-packs/windows-installer-report.json"
|
||||
)
|
||||
)
|
||||
});
|
||||
}
|
||||
throw new Error("命令必须是 build、verify 或 windows-installer");
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./builder.js";
|
||||
export * from "./recipe.js";
|
||||
export * from "./windows-installer.js";
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawn } from "node:child_process";
|
||||
import { lstat, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { getMakeNsisPath } from "app-builder-lib/out/toolsets/windows.js";
|
||||
import { fontPackManifestSchema } from "@md-to-pdf/font-pack-registry";
|
||||
import { verifyFontPack } from "./builder.js";
|
||||
|
||||
export const WINDOWS_FONT_PACK_ARCHITECTURE = "x86_64";
|
||||
|
||||
export interface BuildWindowsFontPackInstallerOptions {
|
||||
packRoot: string;
|
||||
appVersion: string;
|
||||
packId: string;
|
||||
packVersion: string;
|
||||
scriptPath: string;
|
||||
outputDirectory: string;
|
||||
iconPath?: string;
|
||||
reportPath?: string;
|
||||
fontPackRoot?: string;
|
||||
managerRoot?: string;
|
||||
registerUninstall?: boolean;
|
||||
}
|
||||
|
||||
export interface WindowsFontPackInstallerReport {
|
||||
packId: string;
|
||||
packVersion: string;
|
||||
artifactPath: string;
|
||||
artifactName: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
nsisPath: string;
|
||||
}
|
||||
|
||||
export interface CreateDesktopCompanionIncludeOptions {
|
||||
installerPath: string;
|
||||
baseIncludePath: string;
|
||||
outputPath: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
function sha256(content: Uint8Array) {
|
||||
return createHash("sha256").update(content).digest("hex").toUpperCase();
|
||||
}
|
||||
|
||||
function assertDefineValue(value: string, label: string) {
|
||||
if (value.includes('"') || value.includes("\r") || value.includes("\n")) {
|
||||
throw new Error(`${label} 包含 NSIS 定义不支持的字符`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function define(name: string, value: string) {
|
||||
return `/D${name}=${assertDefineValue(value, name)}`;
|
||||
}
|
||||
|
||||
export function windowsFontPackInstallerName(packId: string, version: string) {
|
||||
return `md-to-pdf-font-pack-${packId}-${version}-${WINDOWS_FONT_PACK_ARCHITECTURE}-Setup.exe`;
|
||||
}
|
||||
|
||||
export function createWindowsFontPackInstallerArguments(options: {
|
||||
packDirectory: string;
|
||||
packId: string;
|
||||
packVersion: string;
|
||||
packName: string;
|
||||
licensePath: string;
|
||||
outputPath: string;
|
||||
scriptPath: string;
|
||||
iconPath?: string;
|
||||
fontPackRoot?: string;
|
||||
managerRoot?: string;
|
||||
registerUninstall?: boolean;
|
||||
}) {
|
||||
const argumentsList = [
|
||||
"/V2",
|
||||
"/INPUTCHARSET",
|
||||
"UTF8",
|
||||
define("PACK_DIRECTORY", resolve(options.packDirectory)),
|
||||
define("PACK_ID", options.packId),
|
||||
define("PACK_VERSION", options.packVersion),
|
||||
define("PACK_NAME", options.packName),
|
||||
define("PACK_LICENSE_FILE", resolve(options.licensePath)),
|
||||
define("OUTPUT_FILE", resolve(options.outputPath))
|
||||
];
|
||||
if (options.iconPath) {
|
||||
argumentsList.push(define("ICON_FILE", resolve(options.iconPath)));
|
||||
}
|
||||
if (options.fontPackRoot) {
|
||||
argumentsList.push(define("FONT_PACK_ROOT", resolve(options.fontPackRoot)));
|
||||
}
|
||||
if (options.managerRoot) {
|
||||
argumentsList.push(define("MANAGER_ROOT", resolve(options.managerRoot)));
|
||||
}
|
||||
if (options.registerUninstall !== false) {
|
||||
argumentsList.push("/DREGISTER_UNINSTALL");
|
||||
}
|
||||
argumentsList.push(resolve(options.scriptPath));
|
||||
return argumentsList;
|
||||
}
|
||||
|
||||
async function runProcess(
|
||||
executable: string,
|
||||
argumentsList: string[],
|
||||
environment?: NodeJS.ProcessEnv
|
||||
) {
|
||||
await new Promise<void>((resolvePromise, reject) => {
|
||||
const child = spawn(executable, argumentsList, {
|
||||
env: environment,
|
||||
stdio: "inherit",
|
||||
windowsHide: true
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`NSIS 编译失败:${signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildWindowsFontPackInstaller(
|
||||
options: BuildWindowsFontPackInstallerOptions
|
||||
): Promise<WindowsFontPackInstallerReport> {
|
||||
if (process.platform !== "win32") {
|
||||
throw new Error("字体包 NSIS 安装器只能在 Windows 上构建");
|
||||
}
|
||||
const verified = await verifyFontPack({
|
||||
root: options.packRoot,
|
||||
appVersion: options.appVersion,
|
||||
packId: options.packId,
|
||||
packVersion: options.packVersion
|
||||
});
|
||||
const packDirectory = resolve(verified.outputDirectory);
|
||||
const manifestPath = join(packDirectory, "font-pack.json");
|
||||
const manifest = fontPackManifestSchema.parse(
|
||||
JSON.parse(await readFile(manifestPath, "utf8")) as unknown
|
||||
);
|
||||
const scriptPath = resolve(options.scriptPath);
|
||||
const scriptStatus = await lstat(scriptPath);
|
||||
if (!scriptStatus.isFile() || scriptStatus.isSymbolicLink()) {
|
||||
throw new Error("字体包 NSIS 脚本不是受信任的普通文件");
|
||||
}
|
||||
const outputDirectory = resolve(options.outputDirectory);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
const artifactName = windowsFontPackInstallerName(
|
||||
options.packId,
|
||||
options.packVersion
|
||||
);
|
||||
const artifactPath = join(outputDirectory, artifactName);
|
||||
const nsis = await getMakeNsisPath(null);
|
||||
const argumentsList = createWindowsFontPackInstallerArguments({
|
||||
packDirectory,
|
||||
packId: manifest.id,
|
||||
packVersion: manifest.version,
|
||||
packName: manifest.name,
|
||||
licensePath: join(packDirectory, manifest.licenseFile),
|
||||
outputPath: artifactPath,
|
||||
scriptPath,
|
||||
...(options.iconPath ? { iconPath: options.iconPath } : {}),
|
||||
...(options.fontPackRoot ? { fontPackRoot: options.fontPackRoot } : {}),
|
||||
...(options.managerRoot ? { managerRoot: options.managerRoot } : {}),
|
||||
...(options.registerUninstall !== undefined
|
||||
? { registerUninstall: options.registerUninstall }
|
||||
: {})
|
||||
});
|
||||
await runProcess(nsis.path, argumentsList, {
|
||||
...process.env,
|
||||
...nsis.env
|
||||
});
|
||||
const artifact = await readFile(artifactPath);
|
||||
const artifactStatus = await stat(artifactPath);
|
||||
const report: WindowsFontPackInstallerReport = {
|
||||
packId: manifest.id,
|
||||
packVersion: manifest.version,
|
||||
artifactPath,
|
||||
artifactName,
|
||||
bytes: artifactStatus.size,
|
||||
sha256: sha256(artifact),
|
||||
nsisPath: nsis.path
|
||||
};
|
||||
if (options.reportPath) {
|
||||
const reportPath = resolve(options.reportPath);
|
||||
await mkdir(dirname(reportPath), { recursive: true });
|
||||
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
export async function createDesktopCompanionInclude(
|
||||
options: CreateDesktopCompanionIncludeOptions
|
||||
) {
|
||||
const installerPath = resolve(options.installerPath);
|
||||
const status = await lstat(installerPath);
|
||||
if (!status.isFile() || status.isSymbolicLink()) {
|
||||
throw new Error("字体包伴随安装器不是受信任的普通文件");
|
||||
}
|
||||
const installerHash = sha256(await readFile(installerPath));
|
||||
const include = [
|
||||
`!define FONT_PACK_COMPANION_FILE "${assertDefineValue(basename(installerPath), "伴随安装器文件名")}"`,
|
||||
`!define FONT_PACK_COMPANION_SHA256 "${installerHash}"`,
|
||||
`!define FONT_PACK_COMPANION_NAME "${assertDefineValue(options.displayName, "伴随安装器名称")}"`,
|
||||
`!include "${assertDefineValue(resolve(options.baseIncludePath), "基础安装脚本路径")}"`,
|
||||
""
|
||||
].join("\n");
|
||||
const outputPath = resolve(options.outputPath);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, include, "utf8");
|
||||
return {
|
||||
outputPath,
|
||||
installerPath,
|
||||
installerHash
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createDesktopCompanionInclude,
|
||||
createWindowsFontPackInstallerArguments,
|
||||
windowsFontPackInstallerName
|
||||
} from "../src/index.js";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
const repositoryRoot = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
temporaryDirectories.splice(0).map((directory) =>
|
||||
rm(directory, { recursive: true, force: true })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe("Windows 字体包安装器构建", () => {
|
||||
it("保持用户级原子安装和精确卸载边界", async () => {
|
||||
const script = await readFile(
|
||||
join(repositoryRoot, "font-packs", "windows", "font-pack-installer.nsi"),
|
||||
"utf8"
|
||||
);
|
||||
expect(script).toContain(
|
||||
'$LOCALAPPDATA\\md-to-pdf\\font-packs'
|
||||
);
|
||||
expect(script).toContain(".installing-${PACK_ID}");
|
||||
expect(script).toContain(".backup-${PACK_ID}");
|
||||
expect(script).toContain('Rename "$R0\\${PACK_ID}" "$R1"');
|
||||
expect(script).toContain(
|
||||
'RMDir /r "${FONT_PACK_ROOT}\\${PACK_ID}"'
|
||||
);
|
||||
expect(script).not.toContain("Windows\\Fonts");
|
||||
});
|
||||
|
||||
it("生成稳定发行文件名和完整 NSIS 定义", () => {
|
||||
expect(windowsFontPackInstallerName("mdtp-serif-sc", "1.0.0")).toBe(
|
||||
"md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe"
|
||||
);
|
||||
const args = createWindowsFontPackInstallerArguments({
|
||||
packDirectory: "C:/pack",
|
||||
packId: "mdtp-serif-sc",
|
||||
packVersion: "1.0.0",
|
||||
packName: "测试字体包",
|
||||
licensePath: "C:/pack/LICENSE.txt",
|
||||
outputPath: "C:/out/setup.exe",
|
||||
scriptPath: "C:/repo/font-pack-installer.nsi",
|
||||
registerUninstall: false
|
||||
});
|
||||
expect(args).toContain("/DPACK_ID=mdtp-serif-sc");
|
||||
expect(args).toContain("/DPACK_VERSION=1.0.0");
|
||||
expect(args.slice(0, 3)).toEqual(["/V2", "/INPUTCHARSET", "UTF8"]);
|
||||
expect(args).not.toContain("/DREGISTER_UNINSTALL");
|
||||
expect(args.at(-1)).toMatch(/font-pack-installer\.nsi$/u);
|
||||
});
|
||||
|
||||
it("生成绑定同目录文件名与 SHA-256 的桌面 include", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "mdtp-font-companion-"));
|
||||
temporaryDirectories.push(root);
|
||||
const installer = join(root, "font-setup.exe");
|
||||
const baseInclude = join(root, "installer.nsh");
|
||||
const output = join(root, "generated", "companion.nsh");
|
||||
await Promise.all([
|
||||
writeFile(installer, "installer", "utf8"),
|
||||
writeFile(baseInclude, "!macro customInstall\n!macroend\n", "utf8")
|
||||
]);
|
||||
const result = await createDesktopCompanionInclude({
|
||||
installerPath: installer,
|
||||
baseIncludePath: baseInclude,
|
||||
outputPath: output,
|
||||
displayName: "测试字体包"
|
||||
});
|
||||
const expected = createHash("sha256")
|
||||
.update("installer")
|
||||
.digest("hex")
|
||||
.toUpperCase();
|
||||
const content = await readFile(output, "utf8");
|
||||
expect(result.installerHash).toBe(expected);
|
||||
expect(content).toContain('FONT_PACK_COMPANION_FILE "font-setup.exe"');
|
||||
expect(content).toContain(`FONT_PACK_COMPANION_SHA256 "${expected}"`);
|
||||
expect(content).toContain(baseInclude);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { copyFile, mkdir, readFile } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createDesktopCompanionInclude } from "../packages/font-pack-builder/dist/index.js";
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const desktopRoot = join(repositoryRoot, "apps", "desktop");
|
||||
const desktopManifest = JSON.parse(
|
||||
await readFile(join(desktopRoot, "package.json"), "utf8")
|
||||
);
|
||||
const fontInstallerName =
|
||||
"md-to-pdf-font-pack-mdtp-serif-sc-1.0.0-x86_64-Setup.exe";
|
||||
const fontInstallerPath = join(
|
||||
repositoryRoot,
|
||||
"output",
|
||||
"font-packs",
|
||||
"windows",
|
||||
fontInstallerName
|
||||
);
|
||||
const generatedInclude = join(
|
||||
repositoryRoot,
|
||||
"output",
|
||||
"font-packs",
|
||||
"desktop-companion.nsh"
|
||||
);
|
||||
|
||||
await createDesktopCompanionInclude({
|
||||
installerPath: fontInstallerPath,
|
||||
baseIncludePath: join(desktopRoot, "build", "installer.nsh"),
|
||||
outputPath: generatedInclude,
|
||||
displayName: "MdTP 中文宋体兼容包 1.0.0"
|
||||
});
|
||||
|
||||
const npmCli =
|
||||
process.env.npm_execpath ||
|
||||
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
||||
await new Promise((resolvePromise, reject) => {
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[npmCli, "run", "make", "-w", "@md-to-pdf/desktop"],
|
||||
{
|
||||
cwd: repositoryRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
MD_TO_PDF_NSIS_INCLUDE: generatedInclude
|
||||
},
|
||||
stdio: "inherit",
|
||||
windowsHide: true
|
||||
}
|
||||
);
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolvePromise();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`桌面安装器构建失败:${signal ? `signal ${signal}` : `exit ${code ?? "unknown"}`}`
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const desktopOutput = join(desktopRoot, "out", `v${desktopManifest.version}`);
|
||||
await mkdir(desktopOutput, { recursive: true });
|
||||
await copyFile(fontInstallerPath, join(desktopOutput, fontInstallerName));
|
||||
process.stdout.write(
|
||||
`主安装器与字体包伴随安装器已生成:${desktopOutput}\n`
|
||||
);
|
||||
Reference in New Issue
Block a user