Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 93 additions & 74 deletions scripts/build.cmd
Original file line number Diff line number Diff line change
@@ -1,127 +1,139 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion

:: 先切到仓库根目录
cd /d "%~dp0.."

echo ========================================
echo MD++ 打包构建脚本
echo MD++ Build Script
echo ========================================
echo.

:: 读取当前 package.json 版本号
:: read current version from package.json
for /f %%v in ('node -p "require('./package.json').version"') do set PKG_VER=%%v

echo 当前版本: !PKG_VER!
echo Current version: !PKG_VER!
echo.

:: 询问版本号
set /p INPUT_VER="请输入要发布的版本号 (直接回车使用 !PKG_VER!): "
set /p INPUT_VER="Enter release version (press Enter to use !PKG_VER!): "
if "!INPUT_VER!"=="" set INPUT_VER=!PKG_VER!

echo.
echo ========================================
echo 即将构建 MD++ v!INPUT_VER!
echo 目标: msi + nsis 安装包
echo Building MD++ v!INPUT_VER!
echo Targets: nsis + msi
echo ========================================
echo.

set /p CONFIRM="确认构建? (y/N): "
set /p CONFIRM="Confirm build? (y/N): "
if /i not "!CONFIRM!"=="y" (
echo 已取消构建。
echo Build cancelled.
pause
exit /b 0
)

:: ============================================
:: Step 1: Sync version
:: ============================================
echo.
echo [1/4] 同步版本号...
echo [1/3] Syncing version (source: package.json)...

:: 用 node 更新版本号(避免 PowerShell ConvertTo-Json 破坏编码)
if not "!INPUT_VER!"=="!PKG_VER!" (
echo 更新 package.json: !PKG_VER! -^> !INPUT_VER!
echo Updating package.json: !PKG_VER! -^> !INPUT_VER!
node -e "const fs=require('fs'),p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version='!INPUT_VER!';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\r\n')"
)

:: 同步 tauri.conf.json 版本号
node -e "const fs=require('fs'),t=JSON.parse(fs.readFileSync('src-tauri/tauri.conf.json','utf8'));t.version='!INPUT_VER!';fs.writeFileSync('src-tauri/tauri.conf.json',JSON.stringify(t,null,2)+'\r\n')"
echo 已同步 tauri.conf.json 版本号: !INPUT_VER!

:: 同步 Cargo.toml 版本号
call pnpm sync-version
if %ERRORLEVEL% neq 0 (
echo 版本同步失败! 错误码: %ERRORLEVEL%
echo ERROR: Version sync failed!
pause
exit /b %ERRORLEVEL%
)
echo 已同步 Cargo.toml 版本号: !INPUT_VER!
echo All files synced to v!INPUT_VER!

echo [2/4] 清理旧的构建产物...
if exist "src-tauri\target\release\bundle\msi" rmdir /s /q "src-tauri\target\release\bundle\msi" 2>nul
if exist "src-tauri\target\release\wix" rmdir /s /q "src-tauri\target\release\wix" 2>nul
:: ============================================
:: Step 2: Build app + NSIS installer
:: ============================================
echo.
echo [2/3] Building app + NSIS installer...

echo [3/4] 编译 Rust 后端 + 前端...
call pnpm build
if %ERRORLEVEL% neq 0 (
echo 前端构建失败!
exit /b %ERRORLEVEL%
:: clean old nsis output to avoid conflicts
if exist "src-tauri\target\release\bundle\nsis" rmdir /s /q "src-tauri\target\release\bundle\nsis" 2>nul
if exist "src-tauri\target\release\nsis" rmdir /s /q "src-tauri\target\release\nsis" 2>nul

:: Tauri builds frontend + Rust + NSIS in one go
call pnpm tauri build --bundles nsis
set BUILD_ERR=%ERRORLEVEL%

:: check if NSIS output exists regardless of exit code
set OUT_NSIS=
for %%f in ("src-tauri\target\release\bundle\nsis\*.exe") do set OUT_NSIS=%%f
if "!OUT_NSIS!"=="" (
for %%f in ("src-tauri\target\release\bundle\nsis\*setup*.exe") do set OUT_NSIS=%%f
)
echo 前端构建成功。

echo 编译 Rust 后端 (release)...
cargo build --release --manifest-path src-tauri\Cargo.toml
if %ERRORLEVEL% neq 0 (
echo Rust 编译失败!
exit /b %ERRORLEVEL%
if "!OUT_NSIS!"=="" (
echo ERROR: NSIS build failed (exit code: %BUILD_ERR%).
echo No .exe found in bundle\nsis\
pause
exit /b 1
)
echo Rust 编译成功。
echo NSIS: !OUT_NSIS!

echo [4/4] 打包 MSI 安装包...
:: ============================================
:: Step 3: Build MSI installer (manual WiX)
:: ============================================
echo.
echo [3/3] Building MSI installer...

:: 找到 WiX 工具路径
set WIX_DIR=%LOCALAPPDATA%\tauri\WixTools314
if not exist "%WIX_DIR%\light.exe" (
echo 错误: 找不到 WiX 工具! 请安装 Tauri CLI 后重试。
echo 路径: %WIX_DIR%\light.exe
exit /b 1
echo WiX tools not found at %WIX_DIR%\light.exe
echo Skipping MSI. Install Tauri CLI to enable MSI builds.
goto :done
)

:: 生成 WiX 文件 (tauri bundler 的 candle/light 前置步骤)
:: 先用 tauri build --bundles 仅编译不打包
:: 然后手动运行 candle + light
set BUNDLE_DIR=src-tauri\target\release
set WIX_SRC=%BUNDLE_DIR%\wix\x64

echo 正在准备 WiX 源文件...
:: 调用 Tauri 生成 WiX 配置(candle + light 文件)
call pnpm tauri build --bundles msi 2>&1 | findstr /v "failed light.exe"

:: 如果上面失败(WiX 步骤失败),我们手动构建 MSI
if not exist "%WIX_SRC%\main.wixobj" (
echo Tauri 未生成 .wixobj,尝试直接通过 cargo-bundle...

:: 使用 tauri-bundler 或手动 candle
if exist "%WIX_SRC%\main.wxs" (
echo 运行 candle.exe...
"%WIX_DIR%\candle.exe" -arch x64 -dBuildVersion="!INPUT_VER!" -out "%WIX_SRC%\" "%WIX_SRC%\main.wxs"
if %ERRORLEVEL% neq 0 (
echo candle 失败!
exit /b %ERRORLEVEL%
)
) else (
echo 错误: 找不到 main.wxs!
exit /b 1
)
:: clean old wix output
if exist "%BUNDLE_DIR%\wix" rmdir /s /q "%BUNDLE_DIR%\wix" 2>nul
if exist "%BUNDLE_DIR%\bundle\msi" rmdir /s /q "%BUNDLE_DIR%\bundle\msi" 2>nul

:: let Tauri generate WiX source files (app already built, this is fast)
echo Generating WiX source files...
call pnpm tauri build --bundles msi >nul 2>&1

if not exist "%WIX_SRC%\main.wxs" (
echo ERROR: main.wxs not generated. Skipping MSI.
goto :done
)

:: patch desktop shortcut to use ProductIcon explicitly
echo Patching desktop shortcut icon...
node scripts\patch-wxs.mjs "%WIX_SRC%\main.wxs" 2>nul

:: ensure output dir exists and no stale .wixobj
if not exist "%WIX_SRC%" mkdir "%WIX_SRC%"
if exist "%WIX_SRC%\main.wixobj" del "%WIX_SRC%\main.wixobj"

:: run candle.exe (compile .wxs -> .wixobj)
echo Running candle.exe...
"%WIX_DIR%\candle.exe" -arch x64 -dBuildVersion="!INPUT_VER!" -out "%WIX_SRC%\\" "%WIX_SRC%\main.wxs"
if %ERRORLEVEL% neq 0 (
echo ERROR: candle.exe failed!
pause
exit /b %ERRORLEVEL%
)

:: 修复 locale.wxl 的 codepage(中文需要 936 而非 1252
:: fix locale codepage for Chinese (936 instead of 1252)
set WXL=%WIX_SRC%\locale.wxl
if exist "%WXL%" (
echo 修复 WiX locale codepage 为 936 (支持中文)...
powershell -NoProfile -Command ^
"$xml=[xml](Get-Content '%WXL%');$xml.WixLocalization.Codepage='936';$xml.WixLocalization.SelectSingleNode('//*[local-name()=""String"" and @Id=""TauriCodepage""]').InnerText='936';$xml.Save('%WXL%')"
echo Fixing locale codepage to 936...
node scripts\fix-locale.mjs "%WXL%" 2>nul
)

:: 运行 light.exe 生成 MSI
echo 运行 light.exe 生成 MSI...
:: run light.exe (link .wixobj -> .msi)
echo Running light.exe...
set OUT_MSI=%BUNDLE_DIR%\bundle\msi\md++_!INPUT_VER!_x64_en-US.msi
if not exist "%BUNDLE_DIR%\bundle\msi" mkdir "%BUNDLE_DIR%\bundle\msi"

Expand All @@ -132,15 +144,22 @@ if not exist "%BUNDLE_DIR%\bundle\msi" mkdir "%BUNDLE_DIR%\bundle\msi"
"%WIX_SRC%\main.wixobj"

if %ERRORLEVEL% neq 0 (
echo.
echo light.exe 打包失败! 错误码: %ERRORLEVEL%
echo ERROR: light.exe failed!
pause
exit /b %ERRORLEVEL%
)
echo MSI: %OUT_MSI%

:: ============================================
:: Done
:: ============================================
:done
echo.
echo ========================================
echo 构建完成! 版本: v!INPUT_VER!
echo MSI: %OUT_MSI%
echo Build complete: v%INPUT_VER%
if not "!OUT_NSIS!"=="" echo NSIS: !OUT_NSIS!
if exist "%OUT_MSI%" echo MSI: %OUT_MSI%
echo ========================================

pause
endlocal
31 changes: 31 additions & 0 deletions scripts/fix-locale.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// fix-locale.mjs — 修复 WiX locale.wxl 的 codepage 为 936(支持中文 GBK)
import { readFileSync, writeFileSync } from 'fs';

const wxlPath = process.argv[2];
if (!wxlPath) {
console.error('Usage: node fix-locale.mjs <path-to-locale.wxl>');
process.exit(1);
}

let content = readFileSync(wxlPath, 'utf8');

// 1. Set Codepage attribute on <WixLocalization> root element
content = content.replace(
/(<WixLocalization\s+[^>]*?)>/,
(match, before) => {
if (before.includes('Codepage=')) {
// Update existing Codepage
return before.replace(/Codepage="\d+"/, 'Codepage="936"') + '>';
}
return before + ' Codepage="936">';
}
);

// 2. Set TauriCodepage string value
content = content.replace(
/<String Id="TauriCodepage">\d+<\/String>/,
'<String Id="TauriCodepage">936</String>'
);

writeFileSync(wxlPath, content);
console.log(' ✓ Locale codepage set to 936');
38 changes: 38 additions & 0 deletions scripts/installer.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
!macro NSIS_HOOK_POSTINSTALL
; Make md++ visible in Windows "Open with" and Default Apps.
WriteRegStr HKCU "Software\Classes\Applications\md++.exe" "FriendlyAppName" "md++"
WriteRegStr HKCU "Software\Classes\Applications\md++.exe\DefaultIcon" "" '"$INSTDIR\md++.exe",0'
WriteRegStr HKCU "Software\Classes\Applications\md++.exe\shell\open\command" "" '"$INSTDIR\md++.exe" "%1"'
WriteRegStr HKCU "Software\Classes\Applications\md++.exe\SupportedTypes" ".md" ""
WriteRegStr HKCU "Software\Classes\Applications\md++.exe\SupportedTypes" ".markdown" ""

; ProgID used by the installer and the in-app default-program command.
WriteRegStr HKCU "Software\Classes\MdppMarkdown" "" "Markdown 文件"
WriteRegStr HKCU "Software\Classes\MdppMarkdown" "FriendlyTypeName" "Markdown 文件"
WriteRegStr HKCU "Software\Classes\MdppMarkdown\DefaultIcon" "" '"$INSTDIR\md++.exe",0'
WriteRegStr HKCU "Software\Classes\MdppMarkdown\shell" "" "open"
WriteRegStr HKCU "Software\Classes\MdppMarkdown\shell\open" "" "用 md++ 打开"
WriteRegStr HKCU "Software\Classes\MdppMarkdown\shell\open\command" "" '"$INSTDIR\md++.exe" "%1"'

; Register direct associations and Open With candidates.
WriteRegStr HKCU "Software\Classes\.md" "" "MdppMarkdown"
WriteRegStr HKCU "Software\Classes\.markdown" "" "MdppMarkdown"
WriteRegStr HKCU "Software\Classes\.md\OpenWithProgids" "MdppMarkdown" ""
WriteRegStr HKCU "Software\Classes\.markdown\OpenWithProgids" "MdppMarkdown" ""
WriteRegStr HKCU "Software\Classes\.md\OpenWithList\md++.exe" "" ""
WriteRegStr HKCU "Software\Classes\.markdown\OpenWithList\md++.exe" "" ""
WriteRegStr HKCU "Software\Classes\.md\shell\OpenWithMdpp" "" "用 md++ 打开"
WriteRegStr HKCU "Software\Classes\.md\shell\OpenWithMdpp\command" "" '"$INSTDIR\md++.exe" "%1"'
WriteRegStr HKCU "Software\Classes\.markdown\shell\OpenWithMdpp" "" "用 md++ 打开"
WriteRegStr HKCU "Software\Classes\.markdown\shell\OpenWithMdpp\command" "" '"$INSTDIR\md++.exe" "%1"'

; Capabilities entry shown in Windows Default Apps settings.
WriteRegStr HKCU "Software\md++\Capabilities" "ApplicationName" "md++"
WriteRegStr HKCU "Software\md++\Capabilities" "ApplicationDescription" "Windows 轻量便捷的 Markdown 阅读器"
WriteRegStr HKCU "Software\md++\Capabilities\FileAssociations" ".md" "MdppMarkdown"
WriteRegStr HKCU "Software\md++\Capabilities\FileAssociations" ".markdown" "MdppMarkdown"
WriteRegStr HKCU "Software\RegisteredApplications" "md++" "Software\md++\Capabilities"

; 通知系统刷新文件关联
System::Call 'Shell32::SHChangeNotify(i 0x08000000, i 0x1000, i 0, i 0)'
!macroend
21 changes: 21 additions & 0 deletions scripts/patch-wxs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// patch-wxs.mjs — 修复 Tauri 生成的 main.wxs 桌面快捷方式图标
import { readFileSync, writeFileSync } from 'fs';

const wxsPath = process.argv[2];
if (!wxsPath) {
console.error('Usage: node patch-wxs.mjs <path-to-main.wxs>');
process.exit(1);
}

let content = readFileSync(wxsPath, 'utf8');

// 给桌面快捷方式添加 Icon="ProductIcon" 属性
// Tauri 生成的桌面快捷方式没有显式图标,依赖 exe 文件图标,
// 在某些 Windows 版本上会导致快捷方式图标显示为通用图标。
content = content.replace(
/(<Shortcut\s+Id="ApplicationDesktopShortcut"[^>]*?)(\s*\/>)/,
'$1 Icon="ProductIcon"$2'
);

writeFileSync(wxsPath, content);
console.log(' ✓ 桌面快捷方式图标已修复');
12 changes: 12 additions & 0 deletions scripts/uninstaller.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
!macro NSIS_HOOK_PREUNINSTALL
; 卸载时清理注册表
DeleteRegKey HKCU "Software\Classes\.md\shell\OpenWithMdpp"
DeleteRegKey HKCU "Software\Classes\.markdown\shell\OpenWithMdpp"
DeleteRegKey HKCU "Software\Classes\Applications\md++.exe"
DeleteRegKey HKCU "Software\Classes\MdppMarkdown"
DeleteRegValue HKCU "Software\RegisteredApplications" "md++"
DeleteRegKey HKCU "Software\md++"
; 只清理我们设置的默认值,不删除 .md 和 .markdown 的其他关联
; 通知系统刷新
System::Call 'Shell32::SHChangeNotify(i 0x08000000, i 0x1000, i 0, i 0)'
!macroend
12 changes: 10 additions & 2 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"active": true,
"targets": [
"dmg",
"msi"
"msi",
"nsis"
],
"icon": [
"icons/32x32.png",
Expand All @@ -42,6 +43,13 @@
"icons/icon.icns",
"icons/icon.ico"
],
"windows": {
"nsis": {
"installMode": "currentUser",
"installerIcon": "icons/icon.ico",
"installerHooks": "../scripts/installer.nsh"
}
},
"macOS": {
"minimumSystemVersion": "11.0",
"infoPlist": "Info.plist"
Expand All @@ -53,7 +61,7 @@
"markdown"
],
"mimeType": "text/markdown",
"description": "Markdown 文件"
"description": "Markdown File"
}
]
}
Expand Down
Loading