registry/src 被 cargo gc 清理后改为从 tauri-bundler-*.crate 提取 installer.nsi; tauri.conf.json 备份/还原改用字节读写,避免 Windows 把 LF 改成 CRLF。
256 lines
8.6 KiB
Python
256 lines
8.6 KiB
Python
"""Tauri 桌面版编译脚本。
|
||
|
||
用法:
|
||
python scripts/build_tauri.py # 编译当前系统的桌面版
|
||
python scripts/build_tauri.py --platform windows # 指定平台(windows/linux/macos)
|
||
|
||
特性:
|
||
- Windows 安装包内置 WebView2 离线安装器(tauri.conf.json 已配置 offlineInstaller),
|
||
适合无外网、未预装 WebView2 Runtime 的机器。
|
||
- Windows NSIS 安装器默认安装到 D:\\Program Files\\CapacityReport(无 D 盘时回落系统盘)。
|
||
- 跨平台限制:Tauri 无法可靠跨系统交叉编译,--platform 必须与当前系统一致,否则会提示需在目标系统本机构建。
|
||
|
||
产物:dist/desktop/ 下的安装包(Windows: .msi/.exe;Linux: .deb/.AppImage;macOS: .dmg)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import tarfile
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
import _env # noqa: E402
|
||
|
||
TAURI_CONF = _env.SRC_TAURI_DIR / "tauri.conf.json"
|
||
NSIS_HOOK = _env.SRC_TAURI_DIR / "windows" / "nsis-hooks.nsh"
|
||
|
||
INSTALLER_EXTS = {
|
||
"windows": (".msi", ".exe"),
|
||
"linux": (".deb", ".AppImage", ".rpm"),
|
||
"macos": (".dmg", ".app.tar.gz"),
|
||
}
|
||
|
||
|
||
# --------------------------------------------------------------------------- NSIS 定制(Windows 默认装 D 盘)
|
||
|
||
_RESTORE_LINE = " Call RestorePreviousInstallLocation"
|
||
|
||
_INSTALL_DIR_OVERRIDE = """ Call RestorePreviousInstallLocation
|
||
|
||
!if "${INSTALLMODE}" == "perMachine"
|
||
${If} ${FileExists} "D:\\*.*"
|
||
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||
${Else}
|
||
${If} ${RunningX64}
|
||
StrCpy $INSTDIR "$PROGRAMFILES64\\${PRODUCTNAME}"
|
||
${Else}
|
||
StrCpy $INSTDIR "$PROGRAMFILES\\${PRODUCTNAME}"
|
||
${EndIf}
|
||
${EndIf}
|
||
!endif
|
||
"""
|
||
|
||
_ON_INIT_TAIL = """ !if "${INSTALLMODE}" == "both"
|
||
!insertmacro MULTIUSER_INIT
|
||
!endif
|
||
FunctionEnd"""
|
||
|
||
_ON_INIT_OVERRIDE = """ !if "${INSTALLMODE}" == "both"
|
||
!insertmacro MULTIUSER_INIT
|
||
!endif
|
||
|
||
!if "${INSTALLMODE}" == "perMachine"
|
||
${If} ${FileExists} "D:\\*.*"
|
||
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||
${Else}
|
||
${If} ${RunningX64}
|
||
StrCpy $INSTDIR "$PROGRAMFILES64\\${PRODUCTNAME}"
|
||
${Else}
|
||
StrCpy $INSTDIR "$PROGRAMFILES\\${PRODUCTNAME}"
|
||
${EndIf}
|
||
${EndIf}
|
||
!endif
|
||
FunctionEnd"""
|
||
|
||
_RESTORE_FUNCTION = """Function RestorePreviousInstallLocation
|
||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||
StrCmp $4 "" +2 0
|
||
StrCpy $INSTDIR $4
|
||
FunctionEnd"""
|
||
|
||
_RESTORE_OVERRIDE = """Function RestorePreviousInstallLocation
|
||
!if "${INSTALLMODE}" == "perMachine"
|
||
${If} ${FileExists} "D:\\*.*"
|
||
StrCpy $INSTDIR "D:\\Program Files\\${PRODUCTNAME}"
|
||
Return
|
||
${EndIf}
|
||
!endif
|
||
|
||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||
StrCmp $4 "" +2 0
|
||
StrCpy $INSTDIR $4
|
||
FunctionEnd"""
|
||
|
||
|
||
def _cargo_home() -> Path:
|
||
return Path(os.environ.get("CARGO_HOME") or (Path.home() / ".cargo"))
|
||
|
||
|
||
def _read_template_from_src() -> str | None:
|
||
"""优先读取 registry/src 下已解压的 tauri-bundler 模板。"""
|
||
registry = _cargo_home() / "registry" / "src"
|
||
if not registry.exists():
|
||
return None
|
||
candidates = [
|
||
path
|
||
for path in registry.rglob("installer.nsi")
|
||
if "tauri-bundler-" in str(path) and path.parts[-4:] == ("bundle", "windows", "nsis", "installer.nsi")
|
||
]
|
||
if not candidates:
|
||
return None
|
||
return sorted(candidates, reverse=True)[0].read_text(encoding="utf-8")
|
||
|
||
|
||
def _read_template_from_crate() -> str | None:
|
||
"""registry/src 被 cargo gc 清理时,从缓存的 tauri-bundler-*.crate 包内直接解出模板。"""
|
||
cache = _cargo_home() / "registry" / "cache"
|
||
if not cache.exists():
|
||
return None
|
||
crates = sorted(cache.rglob("tauri-bundler-*.crate"), reverse=True)
|
||
member_suffix = "src/bundle/windows/nsis/installer.nsi"
|
||
for crate in crates:
|
||
try:
|
||
with tarfile.open(crate, "r:gz") as archive:
|
||
for member in archive.getmembers():
|
||
if member.name.endswith(member_suffix):
|
||
extracted = archive.extractfile(member)
|
||
if extracted is not None:
|
||
return extracted.read().decode("utf-8")
|
||
except (tarfile.TarError, OSError):
|
||
continue
|
||
return None
|
||
|
||
|
||
def read_nsis_template_text() -> str:
|
||
content = _read_template_from_src() or _read_template_from_crate()
|
||
if content is None:
|
||
_env.die(
|
||
"未找到 Tauri NSIS 模板(installer.nsi)",
|
||
"先运行一次 cargo tauri build 拉取 tauri-bundler。",
|
||
)
|
||
return content
|
||
|
||
|
||
def make_nsis_template(tmp_dir: Path) -> Path:
|
||
content = read_nsis_template_text()
|
||
|
||
hook_path = str(NSIS_HOOK.resolve())
|
||
include_line = f'!include "{hook_path}"'
|
||
content, n = re.subn(
|
||
r'\{\{#if installer_hooks\}\}\s*!include "\{\{installer_hooks\}\}"\s*\{\{/if\}\}',
|
||
lambda _: include_line,
|
||
content,
|
||
)
|
||
if n == 0:
|
||
_env.die("Tauri NSIS 模板已变化:installer_hooks 标记未找到")
|
||
|
||
for marker, replacement, label in (
|
||
(_RESTORE_LINE, _INSTALL_DIR_OVERRIDE, "RestorePreviousInstallLocation 调用点"),
|
||
(_ON_INIT_TAIL, _ON_INIT_OVERRIDE, ".onInit 结尾"),
|
||
(_RESTORE_FUNCTION, _RESTORE_OVERRIDE, "RestorePreviousInstallLocation 函数"),
|
||
):
|
||
if marker not in content:
|
||
_env.die(f"Tauri NSIS 模板已变化:{label}标记未找到")
|
||
content = content.replace(marker, replacement)
|
||
|
||
target = tmp_dir / "tauri-nsis-installer.nsi"
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_text(content.replace("\r\n", "\n"), encoding="utf-8", newline="\n")
|
||
return target
|
||
|
||
|
||
def build_with_windows_nsis(tmp_dir: Path) -> None:
|
||
template = make_nsis_template(tmp_dir)
|
||
# 按字节备份/还原,避免 Windows 文本写入把原文件 LF 改成 CRLF。
|
||
original = TAURI_CONF.read_bytes()
|
||
try:
|
||
config = json.loads(original.decode("utf-8"))
|
||
config["bundle"]["windows"]["nsis"]["template"] = str(template)
|
||
TAURI_CONF.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
_env.remove_path(_env.SRC_TAURI_DIR / "target" / "release" / "bundle")
|
||
_env.step("编译 Tauri 桌面版(cargo tauri build)")
|
||
_env.run(["cargo", "tauri", "build"], cwd=_env.SRC_TAURI_DIR)
|
||
finally:
|
||
TAURI_CONF.write_bytes(original)
|
||
|
||
|
||
def collect_installers(platform: str) -> None:
|
||
bundle_dir = _env.SRC_TAURI_DIR / "target" / "release" / "bundle"
|
||
out_dir = _env.DIST_DIR / "desktop"
|
||
_env.remove_path(out_dir)
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
exts = INSTALLER_EXTS[platform]
|
||
found = 0
|
||
for path in bundle_dir.rglob("*"):
|
||
if path.is_file() and path.name.endswith(exts):
|
||
shutil.copy2(path, out_dir / path.name)
|
||
found += 1
|
||
_env.info(f"安装包:{path.name}")
|
||
if found == 0:
|
||
_env.warn(f"未在 {bundle_dir} 找到安装包产物")
|
||
_env.step(f"桌面版完成:{out_dir}")
|
||
|
||
|
||
def main() -> int:
|
||
_env.setup_console()
|
||
parser = argparse.ArgumentParser(description="编译 Tauri 桌面版")
|
||
parser.add_argument(
|
||
"--platform",
|
||
choices=["current", "windows", "linux", "macos"],
|
||
default="current",
|
||
help="目标平台(默认当前系统)",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
host = _env.host_platform()
|
||
platform = host if args.platform == "current" else args.platform
|
||
if platform != host:
|
||
_env.die(
|
||
f"无法在 {host} 上交叉编译 {platform} 桌面版。",
|
||
f"Tauri 需要在目标系统本机构建,请在 {platform} 机器上运行本脚本。",
|
||
)
|
||
|
||
_env.ensure_rust()
|
||
python = _env.ensure_venv(extra_packages=["pyinstaller"])
|
||
_env.build_frontend(api_base=_env.DESKTOP_API_BASE)
|
||
|
||
tmp = _env.DIST_DIR / ".tmp"
|
||
_env.build_tauri_sidecar(python, tmp / "pyinstaller" / "work", tmp / "pyinstaller" / "dist")
|
||
_env.ensure_tauri_cli()
|
||
|
||
if platform == "windows":
|
||
build_with_windows_nsis(tmp)
|
||
else:
|
||
_env.remove_path(_env.SRC_TAURI_DIR / "target" / "release" / "bundle")
|
||
_env.step("编译 Tauri 桌面版(cargo tauri build)")
|
||
_env.run(["cargo", "tauri", "build"], cwd=_env.SRC_TAURI_DIR)
|
||
|
||
collect_installers(platform)
|
||
|
||
_env.step("清理中间产物")
|
||
_env.remove_path(tmp)
|
||
_env.remove_path(_env.FRONTEND_DIR / "dist")
|
||
_env.remove_path(_env.SRC_TAURI_DIR / "binaries")
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|